6a4241b6957a
subcommand help: show usage when grouped command has no subcommand — output.subcommandHelp renders an aligned {invocation, desc} table; flake/service/store/channel/gen/pkg now print their subcommand list on empty sub or explicit 'help' (e.g. 'nina flake', 'nina flake help'); channel help shows before the flake note; real subcommands still dispatch unchanged — build+tests green, all six groups + explicit help verified
- time
- 2026-06-10 01:11 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- nina community improvements
- parent
- bbbdab1eedc3
↓ nina-6a4241b6.tar.gzatlas at this save
files changed2 files
src/commands.zig+59 −2src/output.zig+10 −0src/commands.zig+59 −2
@@ -124,7 +124,15 @@124124 for (gens) |g| output.historyRow(g);125125 }126126 127+const gen_help = [_][2][]const u8{128+ .{ "list", "list generations" },129+ .{ "current", "show current generation" },130+ .{ "delete <n>", "delete generation n" },131+ .{ "delete old", "delete old generations" },132+};133+127134 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);128136 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);129137 if (std.mem.eql(u8, ctx.sub, "current")) {130138 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);@@ -825,8 +833,20 @@825833 826834 // --- Service commands ---827835 836+const service_help = [_][2][]const u8{837+ .{ "list", "list running services" },838+ .{ "logs <svc>", "show service logs" },839+ .{ "status <svc>", "service status" },840+ .{ "start <svc>", "start a service" },841+ .{ "stop <svc>", "stop a service" },842+ .{ "restart <svc>", "restart a service" },843+ .{ "enable <svc>", "enable at boot" },844+ .{ "disable <svc>", "disable at boot" },845+};846+828847 pub fn service(ctx: Ctx) !void {829- if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {848+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("service", &service_help);849+ if (std.mem.eql(u8, ctx.sub, "list")) {830850 output.serviceListHeader(ctx.machine.name);831851 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};832852 defer ctx.gpa.free(svcs);@@ -858,7 +878,18 @@858878 859879 // --- Flake commands ---860880 881+const flake_help = [_][2][]const u8{882+ .{ "init", "create a new flake.nix" },883+ .{ "update", "update flake inputs" },884+ .{ "update <in>", "update a specific input" },885+ .{ "check", "validate the flake" },886+ .{ "show", "inspect flake outputs" },887+ .{ "lock", "regenerate flake.lock" },888+ .{ "clone <url>", "clone a flake repository" },889+};890+861891 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);862893 if (std.mem.eql(u8, ctx.sub, "update")) {863894 output.flakeUpdateStart();864895 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;@@ -916,7 +947,16 @@916947 917948 // --- Store commands ---918949 950+const store_help = [_][2][]const u8{951+ .{ "info", "store size and counts" },952+ .{ "path <attr>", "store path of a package" },953+ .{ "gc", "collect garbage" },954+ .{ "verify", "verify store integrity" },955+ .{ "repair", "repair corrupted paths" },956+};957+919958 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);920960 if (std.mem.eql(u8, ctx.sub, "gc")) {921961 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);922962 return;@@ -951,10 +991,18 @@951991 952992 // --- Channel commands ---953993 994+const channel_help = [_][2][]const u8{995+ .{ "list", "list channels" },996+ .{ "add <url> [name]", "add a channel" },997+ .{ "remove <name>", "remove a channel" },998+ .{ "update", "update all channels" },999+};1000+9541001 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);9551003 // On a flake-managed system, nix-channel operations generally don't apply.9561004 if (ctx.cfg.flake) output.flakeChannelNote();957- if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {1005+ if (std.mem.eql(u8, ctx.sub, "list")) {9581006 output.channelListHeader(ctx.machine.name);9591007 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);9601008 defer ctx.gpa.free(chans);@@ -1017,7 +1065,16 @@10171065 10181066 // --- Pkg commands ---10191067 1068+const pkg_help = [_][2][]const u8{1069+ .{ "why <pkg>", "what pulled it in" },1070+ .{ "deps <pkg>", "direct dependencies" },1071+ .{ "size <pkg>", "closure size" },1072+ .{ "path <pkg>", "store path" },1073+ .{ "closure <pkg>", "full closure" },1074+};1075+10201076 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);10211078 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;10221079 if (std.mem.eql(u8, ctx.sub, "why")) {10231080 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);src/output.zig+10 −0
@@ -692,6 +692,16 @@692692 p("\n {s}flags: --on <machine> --dry --check --no-apply --last <n> --all --json{s}\n", .{ c(DIM), c(RESET) });693693 }694694 695+// Render a grouped command's subcommand list, shown when the group is invoked696+// with no subcommand or an explicit `help`. Rows are {invocation, description}.697+pub fn subcommandHelp(group: []const u8, rows: []const [2][]const u8) void {698+ p("{s}{s} subcommands\n\n", .{ hdr(), group });699+ for (rows) |row| {700+ p(" {s:<16} {s}{s}{s}\n", .{ row[0], c(DIM), row[1], c(RESET) });701+ }702+ p("\n nina {s} help {s}for this message{s}\n", .{ group, c(DIM), c(RESET) });703+}704+695705 pub fn version() void {696706 p("{s}nina v{s} zig 0.16 zero dependencies\n", .{ hdr(), nina_version.VERSION });697707 }