e923ab8ccb1a
flake/channel/profile/pkg dispatchers reject an unrecognized subcommand with subcommand help and a non-zero exit instead of silently falling off the if-chain and exiting 0; adds a profile_help table (none existed before) and a shared subcommandKnown helper (NINA-012)
- time
- 2026-07-02 13:48 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- fable audit fix — commands.zig
- parent
- 4f0092888eb7
↓ nina-e923ab8c.tar.gzatlas at this save
files changed1 file
src/commands.zig+51 −0src/commands.zig+51 −0
@@ -1793,6 +1793,19 @@17931793 try std.testing.expect(!serviceVerbAllowed("starting")); // no substring match on "start"17941794 }17951795 1796+// True when `sub` matches one of the verbs advertised in a dispatcher's help1797+// table, ignoring any "<arg>"/"[opt]" suffix (e.g. "add <url> [name]" -> "add").1798+// Shared by flake/channel/profile/pkg so an unrecognized subcommand (a typo1799+// like `nina flake updtae`) fails loudly instead of falling off the end of the1800+// if-chain and exiting 0 (NINA-012).1801+fn subcommandKnown(help_rows: []const [2][]const u8, sub: []const u8) bool {1802+ for (help_rows) |row| {1803+ const verb = if (std.mem.indexOfScalar(u8, row[0], ' ')) |i| row[0][0..i] else row[0];1804+ if (std.mem.eql(u8, verb, sub)) return true;1805+ }1806+ return false;1807+}1808+17961809 // --- Flake commands ---17971810 17981811 const flake_help = [_][2][]const u8{@@ -1807,6 +1820,10 @@18071820 18081821 pub fn flake(ctx: Ctx) !void {18091822 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);1823+ if (!subcommandKnown(&flake_help, ctx.sub)) {1824+ output.subcommandHelp("flake", &flake_help, null);1825+ return error.Internal;1826+ }18101827 if (std.mem.eql(u8, ctx.sub, "update")) {18111828 output.flakeUpdateStart();18121829 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;@@ -1931,6 +1948,10 @@19311948 19321949 pub fn channel(ctx: Ctx) !void {19331950 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);1951+ if (!subcommandKnown(&channel_help, ctx.sub)) {1952+ output.subcommandHelp("channel", &channel_help, null);1953+ return error.Internal;1954+ }19341955 // On a flake-managed system, nix-channel operations generally don't apply.19351956 if (flakeSystem(ctx)) output.flakeChannelNote();19361957 if (std.mem.eql(u8, ctx.sub, "list")) {@@ -1960,7 +1981,19 @@19601981 19611982 // --- Profile commands ---19621983 1984+const profile_help = [_][2][]const u8{1985+ .{ "list", "list profile packages" },1986+ .{ "install <attr>", "install a package into your profile" },1987+ .{ "remove <attr>", "remove a package from your profile" },1988+ .{ "upgrade", "upgrade all profile packages" },1989+};1990+19631991 pub fn profile(ctx: Ctx) !void {1992+ if (std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("profile", &profile_help, null);1993+ if (ctx.sub.len > 0 and !subcommandKnown(&profile_help, ctx.sub)) {1994+ output.subcommandHelp("profile", &profile_help, null);1995+ return error.Internal;1996+ }19641997 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {19651998 output.profileListHeader();19661999 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;@@ -2006,6 +2039,10 @@20062039 20072040 pub fn pkg(ctx: Ctx) !void {20082041 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);2042+ if (!subcommandKnown(&pkg_help, ctx.sub)) {2043+ output.subcommandHelp("pkg", &pkg_help, null);2044+ return error.Internal;2045+ }20092046 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;20102047 if (std.mem.eql(u8, ctx.sub, "why")) {20112048 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;@@ -2031,6 +2068,20 @@20312068 if (try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;20322069 return;20332070 }2071+}2072+2073+test "subcommandKnown rejects a garbage subcommand for each of flake/channel/profile/pkg" {2074+ try std.testing.expect(subcommandKnown(&flake_help, "update"));2075+ try std.testing.expect(!subcommandKnown(&flake_help, "updtae"));2076+2077+ try std.testing.expect(subcommandKnown(&channel_help, "add"));2078+ try std.testing.expect(!subcommandKnown(&channel_help, "addd"));2079+2080+ try std.testing.expect(subcommandKnown(&profile_help, "install"));2081+ try std.testing.expect(!subcommandKnown(&profile_help, "instal"));2082+2083+ try std.testing.expect(subcommandKnown(&pkg_help, "closure"));2084+ try std.testing.expect(!subcommandKnown(&pkg_help, "closures"));20342085 }20352086 20362087 // Top-level alias for `nina pkg why <pkg>` — surfaces what pulled a package in