123const std = @import("std");4const Io = std.Io;56const types = @import("types.zig");7const errors = @import("errors.zig");8const config = @import("config.zig");9const commands = @import("commands.zig");10const output = @import("output.zig");11const man = @import("man.zig");1213const Args = struct {14 command: []const u8 = "",15 sub: []const u8 = "",16 rest: []const []const u8 = &.{},17 passthrough: []const []const u8 = &.{}, 18 machine: ?[]const u8 = null,19 version: bool = false,20 dry: bool = false,21 check: bool = false,22 no_apply: bool = false,23 all: bool = false,24 json: bool = false,25 last: u32 = 50,26 user: bool = false,27 yes: bool = false, 28};2930pub fn main(startup: std.process.Init) !void {31 const arena = startup.arena.allocator();32 const gpa = startup.gpa;33 const io = startup.io;3435 36 37 38 39 var cmd_arena = std.heap.ArenaAllocator.init(gpa);40 defer cmd_arena.deinit();41 const ca = cmd_arena.allocator();4243 44 45 46 47 48 var stdout_buf: [16384]u8 = undefined;49 var stdout_fw: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buf);50 output.init(&stdout_fw.interface, io, startup.environ_map, true);51 defer output.flush();5253 const argv = try startup.minimal.args.toSlice(arena);54 const args = try parseArgs(arena, argv);5556 57 58 59 if (args.yes) output.setAutoYes(true);6061 if (args.version) {62 output.version();63 return;64 }6566 67 if (args.command.len == 0 or std.mem.eql(u8, args.command, "help")) {68 output.help();69 return;70 }7172 73 if (std.mem.eql(u8, args.command, "man")) {74 const topic: []const u8 = if (args.rest.len > 0) args.rest[0] else "";75 man.run(io, topic) catch {};76 return;77 }7879 80 if (std.mem.eql(u8, args.command, "setup") or std.mem.eql(u8, args.command, "update")) {81 const dummy_machine: types.Machine = .{ .name = "local", .local = true };82 const dummy_cfg: types.NinaConfig = .{};83 const pre_ctx = commands.Ctx{84 .gpa = ca,85 .io = io,86 .environ = startup.environ_map,87 .machine = dummy_machine,88 .cfg = dummy_cfg,89 .args = args.rest,90 .passthrough = args.passthrough,91 .sub = args.sub,92 .dry = args.dry,93 .check = args.check,94 .no_apply = args.no_apply,95 .all = args.all,96 .last = args.last,97 .json = args.json,98 .user = args.user,99 };100 if (std.mem.eql(u8, args.command, "setup")) {101 commands.setup(ca, io, startup.environ_map) catch |err| {102 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);103 const detail = errors.error_info.getDetail();104 const suggestion = errors.error_info.getSuggestion();105 output.printError(msg, detail, suggestion);106 output.flush();107 std.process.exit(1);108 };109 } else {110 dispatch(pre_ctx, args.command) catch |err| {111 reportError(err, ca);112 output.flush();113 std.process.exit(1);114 };115 }116 return;117 }118119 120 121 const cfg = cfg_blk: {122 if (config.load(arena, io, startup.environ_map)) |c| {123 break :cfg_blk c;124 } else |err| {125 if (err == error.ConfigNotFound and (std.Io.File.stdout().isTty(io) catch false)) {126 commands.firstRunWizard(ca, io, startup.environ_map) catch |wizard_err| {127 const msg = errors.error_info.getMessage() orelse errors.staticMessage(wizard_err);128 const detail = errors.error_info.getDetail();129 const suggestion = errors.error_info.getSuggestion();130 output.printError(msg, detail, suggestion);131 output.flush();132 std.process.exit(1);133 };134 errors.error_info.reset();135 break :cfg_blk config.load(arena, io, startup.environ_map) catch |err2| {136 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err2);137 const detail = errors.error_info.getDetail();138 const suggestion = errors.error_info.getSuggestion();139 output.printError(msg, detail, suggestion);140 output.flush();141 std.process.exit(1);142 };143 }144 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);145 const detail = errors.error_info.getDetail();146 const suggestion = errors.error_info.getSuggestion();147 output.printError(msg, detail, suggestion);148 output.flush();149 std.process.exit(1);150 }151 };152153 output.setTeach(cfg.teach);154155 const machine = config.resolveMachine(&cfg, args.machine) catch |err| {156 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);157 const detail = errors.error_info.getDetail();158 const suggestion = errors.error_info.getSuggestion();159 output.printError(msg, detail, suggestion);160 output.flush();161 std.process.exit(1);162 };163164 const ctx = commands.Ctx{165 .gpa = ca,166 .io = io,167 .environ = startup.environ_map,168 .machine = machine,169 .cfg = cfg,170 .args = args.rest,171 .passthrough = args.passthrough,172 .sub = args.sub,173 .dry = args.dry,174 .check = args.check,175 .no_apply = args.no_apply,176 .all = args.all,177 .last = args.last,178 .json = args.json,179 .user = args.user,180 };181182 dispatch(ctx, args.command) catch |err| {183 reportError(err, ca);184 output.flush();185 std.process.exit(1);186 };187}188189190191192fn reportError(err: anyerror, allocator: std.mem.Allocator) void {193 switch (err) {194 195 error.HookAborted => {},196 197 error.CheckFailed => {},198 error.BuildFailed => {199 const stderr = errors.getBuildStderr();200 const translated = errors.translateError(allocator, stderr);201 output.buildError(translated);202 },203 error.NetworkError, error.SshFailed, error.SearchFailed => {204 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);205 const detail = errors.error_info.getDetail();206 const suggestion = errors.error_info.getSuggestion();207 output.printSystemError(msg, detail, suggestion);208 },209 else => {210 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);211 const detail = errors.error_info.getDetail();212 const suggestion = errors.error_info.getSuggestion();213 output.printError(msg, detail, suggestion);214 },215 }216}217218fn dispatch(ctx: commands.Ctx, cmd: []const u8) !void {219 if (std.mem.eql(u8, cmd, "apply")) return commands.apply(ctx);220 if (std.mem.eql(u8, cmd, "check")) return commands.check(ctx);221 if (std.mem.eql(u8, cmd, "back")) return commands.back(ctx);222 if (std.mem.eql(u8, cmd, "go")) return commands.go(ctx);223 if (std.mem.eql(u8, cmd, "history")) return commands.history(ctx);224 if (std.mem.eql(u8, cmd, "gen")) return commands.genCmd(ctx);225 if (std.mem.eql(u8, cmd, "clean")) return commands.clean(ctx);226 if (std.mem.eql(u8, cmd, "status")) return commands.statusCmd(ctx);227 if (std.mem.eql(u8, cmd, "diff")) return commands.diff(ctx);228 if (std.mem.eql(u8, cmd, "doctor")) return commands.doctor(ctx);229 if (std.mem.eql(u8, cmd, "mood")) return commands.mood(ctx);230 if (std.mem.eql(u8, cmd, "update")) return commands.update(ctx);231 if (std.mem.eql(u8, cmd, "upgrade")) return commands.upgrade(ctx);232 if (std.mem.eql(u8, cmd, "log")) return commands.log(ctx);233 if (std.mem.eql(u8, cmd, "edit")) return commands.edit(ctx);234 if (std.mem.eql(u8, cmd, "fmt")) return commands.fmt(ctx);235 if (std.mem.eql(u8, cmd, "repl")) return commands.repl(ctx);236 if (std.mem.eql(u8, cmd, "develop")) return commands.develop(ctx);237 if (std.mem.eql(u8, cmd, "info")) return commands.info(ctx);238 if (std.mem.eql(u8, cmd, "boot")) return commands.boot(ctx);239 if (std.mem.eql(u8, cmd, "search")) return commands.searchCmd(ctx);240 if (std.mem.eql(u8, cmd, "option")) return commands.optionCmd(ctx);241 if (std.mem.eql(u8, cmd, "install")) return commands.install(ctx);242 if (std.mem.eql(u8, cmd, "remove")) return commands.remove(ctx);243 if (std.mem.eql(u8, cmd, "list")) return commands.list(ctx);244 if (std.mem.eql(u8, cmd, "try")) return commands.tryCmd(ctx);245 if (std.mem.eql(u8, cmd, "service")) return commands.service(ctx);246 if (std.mem.eql(u8, cmd, "flake")) return commands.flake(ctx);247 if (std.mem.eql(u8, cmd, "store")) return commands.store(ctx);248 if (std.mem.eql(u8, cmd, "channel")) return commands.channel(ctx);249 if (std.mem.eql(u8, cmd, "profile")) return commands.profile(ctx);250 if (std.mem.eql(u8, cmd, "pkg")) return commands.pkg(ctx);251 if (std.mem.eql(u8, cmd, "why")) return commands.why(ctx);252 if (std.mem.eql(u8, cmd, "pin")) return commands.pin(ctx);253 if (std.mem.eql(u8, cmd, "unpin")) return commands.unpin(ctx);254 if (std.mem.eql(u8, cmd, "hash")) return commands.hash(ctx);255 if (std.mem.eql(u8, cmd, "fetch")) return commands.fetch(ctx);256 if (std.mem.eql(u8, cmd, "build")) return commands.build(ctx);257 if (std.mem.eql(u8, cmd, "run")) return commands.runCmd(ctx);258 if (std.mem.eql(u8, cmd, "home")) return commands.home(ctx);259 if (std.mem.eql(u8, cmd, "goodbye")) return commands.goodbye(ctx);260 if (std.mem.eql(u8, cmd, "hello")) return commands.hello(ctx);261 if (std.mem.eql(u8, cmd, "help")) {262 output.help();263 return;264 }265266 errors.error_info.setMessage("unknown command: {s}", .{cmd});267 errors.error_info.setSuggestion("nina help", .{});268 return error.Internal;269}270271fn parseArgs(arena: std.mem.Allocator, argv: []const [:0]const u8) !Args {272 var result = Args{};273 var rest: std.ArrayList([]const u8) = .empty;274 var passthrough: std.ArrayList([]const u8) = .empty;275 var i: usize = 1; 276277 278 if (i < argv.len and argv[i][0] != '-') {279 result.command = argv[i];280 i += 1;281 }282283 284 if (i < argv.len and argv[i][0] != '-') {285 const multi_word = isMultiWord(result.command);286 if (multi_word) {287 result.sub = argv[i];288 i += 1;289 }290 }291292 293 294 295 296 var past_separator = false;297 while (i < argv.len) : (i += 1) {298 const a = argv[i];299 if (past_separator) {300 try passthrough.append(arena, a);301 continue;302 }303 if (std.mem.eql(u8, a, "--")) {304 past_separator = true;305 try passthrough.append(arena, a);306 continue;307 }308 if (std.mem.eql(u8, a, "--on") or std.mem.eql(u8, a, "-on")) {309 i += 1;310 if (i < argv.len) result.machine = argv[i];311 } else if (std.mem.eql(u8, a, "--dry")) {312 result.dry = true;313 } else if (std.mem.eql(u8, a, "--check")) {314 result.check = true;315 } else if (std.mem.eql(u8, a, "--version")) {316 result.version = true;317 } else if (std.mem.eql(u8, a, "--no-apply")) {318 result.no_apply = true;319 } else if (std.mem.eql(u8, a, "--all")) {320 result.all = true;321 } else if (std.mem.eql(u8, a, "--user")) {322 result.user = true;323 } else if (std.mem.eql(u8, a, "--json")) {324 result.json = true;325 } else if (std.mem.eql(u8, a, "--yes")) {326 result.yes = true;327 } else if (std.mem.eql(u8, a, "--last")) {328 i += 1;329 if (i < argv.len) result.last = std.fmt.parseInt(u32, argv[i], 10) catch 50;330 } else if (std.mem.eql(u8, a, "-f")) {331 332 try rest.append(arena, "-f");333 } else if (std.mem.eql(u8, a, "--switch")) {334 335 try rest.append(arena, "--switch");336 } else if (std.mem.eql(u8, a, "--dir")) {337 338 try rest.append(arena, "--dir");339 } else if (a.len > 0 and a[0] != '-') {340 try rest.append(arena, a);341 } else {342 343 344 try passthrough.append(arena, a);345 }346 }347348 result.rest = try rest.toOwnedSlice(arena);349 result.passthrough = try passthrough.toOwnedSlice(arena);350 return result;351}352353fn isMultiWord(cmd: []const u8) bool {354 const multi = [_][]const u8{ "service", "flake", "store", "channel", "profile", "pkg", "gen", "develop", "edit", "home" };355 for (multi) |m| {356 if (std.mem.eql(u8, cmd, m)) return true;357 }358 return false;359}360361362363364365test {366 std.testing.refAllDecls(@This());367}368369test "parse args basic" {370 const gpa = std.testing.allocator;371 const argv = [_][:0]const u8{ "nina", "apply", "--dry" };372 const args = try parseArgs(gpa, &argv);373 defer gpa.free(args.rest);374 try std.testing.expectEqualStrings("apply", args.command);375 try std.testing.expect(args.dry);376}377378test "parse args with machine" {379 const gpa = std.testing.allocator;380 const argv = [_][:0]const u8{ "nina", "status", "--on", "build-server" };381 const args = try parseArgs(gpa, &argv);382 defer gpa.free(args.rest);383 try std.testing.expectEqualStrings("status", args.command);384 try std.testing.expectEqualStrings("build-server", args.machine.?);385}386387test "parse args forwards unrecognized flags" {388 const gpa = std.testing.allocator;389 const argv = [_][:0]const u8{ "nina", "flake", "update", "--refresh" };390 const args = try parseArgs(gpa, &argv);391 defer gpa.free(args.rest);392 defer gpa.free(args.passthrough);393 try std.testing.expectEqualStrings("flake", args.command);394 try std.testing.expectEqualStrings("update", args.sub);395 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);396 try std.testing.expectEqualStrings("--refresh", args.passthrough[0]);397}398399test "parse args forwards apply --show-trace" {400 const gpa = std.testing.allocator;401 const argv = [_][:0]const u8{ "nina", "apply", "--show-trace" };402 const args = try parseArgs(gpa, &argv);403 defer gpa.free(args.rest);404 defer gpa.free(args.passthrough);405 try std.testing.expectEqualStrings("apply", args.command);406 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);407 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);408}409410test "parse args forwards check --show-trace" {411 const gpa = std.testing.allocator;412 const argv = [_][:0]const u8{ "nina", "check", "--show-trace" };413 const args = try parseArgs(gpa, &argv);414 defer gpa.free(args.rest);415 defer gpa.free(args.passthrough);416 try std.testing.expectEqualStrings("check", args.command);417 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);418 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);419}420421test "parse args forwards flake check --all-systems" {422 const gpa = std.testing.allocator;423 const argv = [_][:0]const u8{ "nina", "flake", "check", "--all-systems" };424 const args = try parseArgs(gpa, &argv);425 defer gpa.free(args.rest);426 defer gpa.free(args.passthrough);427 try std.testing.expectEqualStrings("flake", args.command);428 try std.testing.expectEqualStrings("check", args.sub);429 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);430 try std.testing.expectEqualStrings("--all-systems", args.passthrough[0]);431}432433test "parse args forwards build --print-build-logs" {434 const gpa = std.testing.allocator;435 const argv = [_][:0]const u8{ "nina", "build", "--print-build-logs" };436 const args = try parseArgs(gpa, &argv);437 defer gpa.free(args.rest);438 defer gpa.free(args.passthrough);439 try std.testing.expectEqualStrings("build", args.command);440 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);441 try std.testing.expectEqualStrings("--print-build-logs", args.passthrough[0]);442}443444test "parse args forwards develop --show-trace" {445 const gpa = std.testing.allocator;446 const argv = [_][:0]const u8{ "nina", "develop", "--show-trace" };447 const args = try parseArgs(gpa, &argv);448 defer gpa.free(args.rest);449 defer gpa.free(args.passthrough);450 try std.testing.expectEqualStrings("develop", args.command);451 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);452 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);453}454455test "parse args forwards run's -- separator and program args" {456 const gpa = std.testing.allocator;457 const argv = [_][:0]const u8{ "nina", "run", "hello", "--", "--version" };458 const args = try parseArgs(gpa, &argv);459 defer gpa.free(args.rest);460 defer gpa.free(args.passthrough);461 try std.testing.expectEqualStrings("run", args.command);462 try std.testing.expectEqual(@as(usize, 1), args.rest.len);463 try std.testing.expectEqualStrings("hello", args.rest[0]);464 try std.testing.expectEqual(@as(usize, 2), args.passthrough.len);465 try std.testing.expectEqualStrings("--", args.passthrough[0]);466 try std.testing.expectEqualStrings("--version", args.passthrough[1]);467}468469test "parse args forwards home apply --show-trace" {470 const gpa = std.testing.allocator;471 const argv = [_][:0]const u8{ "nina", "home", "apply", "--show-trace" };472 const args = try parseArgs(gpa, &argv);473 defer gpa.free(args.rest);474 defer gpa.free(args.passthrough);475 try std.testing.expectEqualStrings("home", args.command);476 try std.testing.expectEqualStrings("apply", args.sub);477 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);478 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);479}480481test "parse subcommand" {482 const gpa = std.testing.allocator;483 const argv = [_][:0]const u8{ "nina", "service", "list" };484 const args = try parseArgs(gpa, &argv);485 defer gpa.free(args.rest);486 try std.testing.expectEqualStrings("service", args.command);487 try std.testing.expectEqualStrings("list", args.sub);488}489490test "parse args recognizes --yes" {491 const gpa = std.testing.allocator;492 const argv = [_][:0]const u8{ "nina", "clean", "--yes" };493 const args = try parseArgs(gpa, &argv);494 defer gpa.free(args.rest);495 defer gpa.free(args.passthrough);496 try std.testing.expectEqualStrings("clean", args.command);497 try std.testing.expect(args.yes);498 try std.testing.expectEqual(@as(usize, 0), args.passthrough.len);499}500501test "parse version flag" {502 const gpa = std.testing.allocator;503 const argv = [_][:0]const u8{ "nina", "--version" };504 const args = try parseArgs(gpa, &argv);505 defer gpa.free(args.rest);506 try std.testing.expect(args.version);507}