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};2829pub fn main(startup: std.process.Init) !void {30 const arena = startup.arena.allocator();31 const gpa = startup.gpa;32 const io = startup.io;3334 35 36 37 38 var cmd_arena = std.heap.ArenaAllocator.init(gpa);39 defer cmd_arena.deinit();40 const ca = cmd_arena.allocator();4142 43 44 45 46 47 var stdout_buf: [16384]u8 = undefined;48 var stdout_fw: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buf);49 output.init(&stdout_fw.interface, io, startup.environ_map, true);50 defer output.flush();5152 const argv = try startup.minimal.args.toSlice(arena);53 const args = try parseArgs(arena, argv);5455 if (args.version) {56 output.version();57 return;58 }5960 61 if (args.command.len == 0 or std.mem.eql(u8, args.command, "help")) {62 output.help();63 return;64 }6566 67 if (std.mem.eql(u8, args.command, "man")) {68 const topic: []const u8 = if (args.rest.len > 0) args.rest[0] else "";69 man.run(io, topic) catch {};70 return;71 }7273 74 if (std.mem.eql(u8, args.command, "setup") or std.mem.eql(u8, args.command, "update")) {75 const dummy_machine: types.Machine = .{ .name = "local", .local = true };76 const dummy_cfg: types.NinaConfig = .{};77 const pre_ctx = commands.Ctx{78 .gpa = ca,79 .io = io,80 .environ = startup.environ_map,81 .machine = dummy_machine,82 .cfg = dummy_cfg,83 .args = args.rest,84 .passthrough = args.passthrough,85 .sub = args.sub,86 .dry = args.dry,87 .check = args.check,88 .no_apply = args.no_apply,89 .all = args.all,90 .last = args.last,91 .json = args.json,92 .user = args.user,93 };94 if (std.mem.eql(u8, args.command, "setup")) {95 commands.setup(ca, io, startup.environ_map) catch |err| {96 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);97 const detail = errors.error_info.getDetail();98 const suggestion = errors.error_info.getSuggestion();99 output.printError(msg, detail, suggestion);100 output.flush();101 std.process.exit(1);102 };103 } else {104 dispatch(pre_ctx, args.command) catch |err| {105 reportError(err, ca);106 output.flush();107 std.process.exit(1);108 };109 }110 return;111 }112113 114 115 const cfg = cfg_blk: {116 if (config.load(arena, io, startup.environ_map)) |c| {117 break :cfg_blk c;118 } else |err| {119 if (err == error.ConfigNotFound and (std.Io.File.stdout().isTty(io) catch false)) {120 commands.firstRunWizard(ca, io, startup.environ_map) catch |wizard_err| {121 const msg = errors.error_info.getMessage() orelse errors.staticMessage(wizard_err);122 const detail = errors.error_info.getDetail();123 const suggestion = errors.error_info.getSuggestion();124 output.printError(msg, detail, suggestion);125 output.flush();126 std.process.exit(1);127 };128 errors.error_info.reset();129 break :cfg_blk config.load(arena, io, startup.environ_map) catch |err2| {130 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err2);131 const detail = errors.error_info.getDetail();132 const suggestion = errors.error_info.getSuggestion();133 output.printError(msg, detail, suggestion);134 output.flush();135 std.process.exit(1);136 };137 }138 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);139 const detail = errors.error_info.getDetail();140 const suggestion = errors.error_info.getSuggestion();141 output.printError(msg, detail, suggestion);142 output.flush();143 std.process.exit(1);144 }145 };146147 output.setTeach(cfg.teach);148149 const machine = config.resolveMachine(&cfg, args.machine) catch |err| {150 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);151 const detail = errors.error_info.getDetail();152 const suggestion = errors.error_info.getSuggestion();153 output.printError(msg, detail, suggestion);154 output.flush();155 std.process.exit(1);156 };157158 const ctx = commands.Ctx{159 .gpa = ca,160 .io = io,161 .environ = startup.environ_map,162 .machine = machine,163 .cfg = cfg,164 .args = args.rest,165 .passthrough = args.passthrough,166 .sub = args.sub,167 .dry = args.dry,168 .check = args.check,169 .no_apply = args.no_apply,170 .all = args.all,171 .last = args.last,172 .json = args.json,173 .user = args.user,174 };175176 dispatch(ctx, args.command) catch |err| {177 reportError(err, ca);178 output.flush();179 std.process.exit(1);180 };181}182183184185186fn reportError(err: anyerror, allocator: std.mem.Allocator) void {187 switch (err) {188 189 error.HookAborted => {},190 191 error.CheckFailed => {},192 error.BuildFailed => {193 const stderr = errors.getBuildStderr();194 const translated = errors.translateError(allocator, stderr);195 output.buildError(translated);196 },197 error.NetworkError, error.SshFailed, error.SearchFailed => {198 const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);199 const detail = errors.error_info.getDetail();200 const suggestion = errors.error_info.getSuggestion();201 output.printSystemError(msg, detail, suggestion);202 },203 else => {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.printError(msg, detail, suggestion);208 },209 }210}211212fn dispatch(ctx: commands.Ctx, cmd: []const u8) !void {213 if (std.mem.eql(u8, cmd, "apply")) return commands.apply(ctx);214 if (std.mem.eql(u8, cmd, "check")) return commands.check(ctx);215 if (std.mem.eql(u8, cmd, "back")) return commands.back(ctx);216 if (std.mem.eql(u8, cmd, "go")) return commands.go(ctx);217 if (std.mem.eql(u8, cmd, "history")) return commands.history(ctx);218 if (std.mem.eql(u8, cmd, "gen")) return commands.genCmd(ctx);219 if (std.mem.eql(u8, cmd, "clean")) return commands.clean(ctx);220 if (std.mem.eql(u8, cmd, "status")) return commands.statusCmd(ctx);221 if (std.mem.eql(u8, cmd, "diff")) return commands.diff(ctx);222 if (std.mem.eql(u8, cmd, "doctor")) return commands.doctor(ctx);223 if (std.mem.eql(u8, cmd, "mood")) return commands.mood(ctx);224 if (std.mem.eql(u8, cmd, "update")) return commands.update(ctx);225 if (std.mem.eql(u8, cmd, "upgrade")) return commands.upgrade(ctx);226 if (std.mem.eql(u8, cmd, "log")) return commands.log(ctx);227 if (std.mem.eql(u8, cmd, "edit")) return commands.edit(ctx);228 if (std.mem.eql(u8, cmd, "fmt")) return commands.fmt(ctx);229 if (std.mem.eql(u8, cmd, "repl")) return commands.repl(ctx);230 if (std.mem.eql(u8, cmd, "develop")) return commands.develop(ctx);231 if (std.mem.eql(u8, cmd, "info")) return commands.info(ctx);232 if (std.mem.eql(u8, cmd, "boot")) return commands.boot(ctx);233 if (std.mem.eql(u8, cmd, "search")) return commands.searchCmd(ctx);234 if (std.mem.eql(u8, cmd, "option")) return commands.optionCmd(ctx);235 if (std.mem.eql(u8, cmd, "install")) return commands.install(ctx);236 if (std.mem.eql(u8, cmd, "remove")) return commands.remove(ctx);237 if (std.mem.eql(u8, cmd, "list")) return commands.list(ctx);238 if (std.mem.eql(u8, cmd, "try")) return commands.tryCmd(ctx);239 if (std.mem.eql(u8, cmd, "service")) return commands.service(ctx);240 if (std.mem.eql(u8, cmd, "flake")) return commands.flake(ctx);241 if (std.mem.eql(u8, cmd, "store")) return commands.store(ctx);242 if (std.mem.eql(u8, cmd, "channel")) return commands.channel(ctx);243 if (std.mem.eql(u8, cmd, "profile")) return commands.profile(ctx);244 if (std.mem.eql(u8, cmd, "pkg")) return commands.pkg(ctx);245 if (std.mem.eql(u8, cmd, "why")) return commands.why(ctx);246 if (std.mem.eql(u8, cmd, "pin")) return commands.pin(ctx);247 if (std.mem.eql(u8, cmd, "unpin")) return commands.unpin(ctx);248 if (std.mem.eql(u8, cmd, "hash")) return commands.hash(ctx);249 if (std.mem.eql(u8, cmd, "fetch")) return commands.fetch(ctx);250 if (std.mem.eql(u8, cmd, "build")) return commands.build(ctx);251 if (std.mem.eql(u8, cmd, "run")) return commands.runCmd(ctx);252 if (std.mem.eql(u8, cmd, "home")) return commands.home(ctx);253 if (std.mem.eql(u8, cmd, "goodbye")) return commands.goodbye(ctx);254 if (std.mem.eql(u8, cmd, "hello")) return commands.hello(ctx);255 if (std.mem.eql(u8, cmd, "help")) {256 output.help();257 return;258 }259260 errors.error_info.setMessage("unknown command: {s}", .{cmd});261 errors.error_info.setSuggestion("nina help", .{});262 return error.Internal;263}264265fn parseArgs(arena: std.mem.Allocator, argv: []const [:0]const u8) !Args {266 var result = Args{};267 var rest: std.ArrayList([]const u8) = .empty;268 var passthrough: std.ArrayList([]const u8) = .empty;269 var i: usize = 1; 270271 272 if (i < argv.len and argv[i][0] != '-') {273 result.command = argv[i];274 i += 1;275 }276277 278 if (i < argv.len and argv[i][0] != '-') {279 const multi_word = isMultiWord(result.command);280 if (multi_word) {281 result.sub = argv[i];282 i += 1;283 }284 }285286 287 288 289 290 var past_separator = false;291 while (i < argv.len) : (i += 1) {292 const a = argv[i];293 if (past_separator) {294 try passthrough.append(arena, a);295 continue;296 }297 if (std.mem.eql(u8, a, "--")) {298 past_separator = true;299 try passthrough.append(arena, a);300 continue;301 }302 if (std.mem.eql(u8, a, "--on") or std.mem.eql(u8, a, "-on")) {303 i += 1;304 if (i < argv.len) result.machine = argv[i];305 } else if (std.mem.eql(u8, a, "--dry")) {306 result.dry = true;307 } else if (std.mem.eql(u8, a, "--check")) {308 result.check = true;309 } else if (std.mem.eql(u8, a, "--version")) {310 result.version = true;311 } else if (std.mem.eql(u8, a, "--no-apply")) {312 result.no_apply = true;313 } else if (std.mem.eql(u8, a, "--all")) {314 result.all = true;315 } else if (std.mem.eql(u8, a, "--user")) {316 result.user = true;317 } else if (std.mem.eql(u8, a, "--json")) {318 result.json = true;319 } else if (std.mem.eql(u8, a, "--last")) {320 i += 1;321 if (i < argv.len) result.last = std.fmt.parseInt(u32, argv[i], 10) catch 50;322 } else if (std.mem.eql(u8, a, "-f")) {323 324 try rest.append(arena, "-f");325 } else if (std.mem.eql(u8, a, "--switch")) {326 327 try rest.append(arena, "--switch");328 } else if (std.mem.eql(u8, a, "--dir")) {329 330 try rest.append(arena, "--dir");331 } else if (a.len > 0 and a[0] != '-') {332 try rest.append(arena, a);333 } else {334 335 336 try passthrough.append(arena, a);337 }338 }339340 result.rest = try rest.toOwnedSlice(arena);341 result.passthrough = try passthrough.toOwnedSlice(arena);342 return result;343}344345fn isMultiWord(cmd: []const u8) bool {346 const multi = [_][]const u8{ "service", "flake", "store", "channel", "profile", "pkg", "gen", "develop", "edit", "home" };347 for (multi) |m| {348 if (std.mem.eql(u8, cmd, m)) return true;349 }350 return false;351}352353354355356357test {358 std.testing.refAllDecls(@This());359}360361test "parse args basic" {362 const gpa = std.testing.allocator;363 const argv = [_][:0]const u8{ "nina", "apply", "--dry" };364 const args = try parseArgs(gpa, &argv);365 defer gpa.free(args.rest);366 try std.testing.expectEqualStrings("apply", args.command);367 try std.testing.expect(args.dry);368}369370test "parse args with machine" {371 const gpa = std.testing.allocator;372 const argv = [_][:0]const u8{ "nina", "status", "--on", "build-server" };373 const args = try parseArgs(gpa, &argv);374 defer gpa.free(args.rest);375 try std.testing.expectEqualStrings("status", args.command);376 try std.testing.expectEqualStrings("build-server", args.machine.?);377}378379test "parse args forwards unrecognized flags" {380 const gpa = std.testing.allocator;381 const argv = [_][:0]const u8{ "nina", "flake", "update", "--refresh" };382 const args = try parseArgs(gpa, &argv);383 defer gpa.free(args.rest);384 defer gpa.free(args.passthrough);385 try std.testing.expectEqualStrings("flake", args.command);386 try std.testing.expectEqualStrings("update", args.sub);387 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);388 try std.testing.expectEqualStrings("--refresh", args.passthrough[0]);389}390391test "parse args forwards apply --show-trace" {392 const gpa = std.testing.allocator;393 const argv = [_][:0]const u8{ "nina", "apply", "--show-trace" };394 const args = try parseArgs(gpa, &argv);395 defer gpa.free(args.rest);396 defer gpa.free(args.passthrough);397 try std.testing.expectEqualStrings("apply", args.command);398 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);399 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);400}401402test "parse args forwards check --show-trace" {403 const gpa = std.testing.allocator;404 const argv = [_][:0]const u8{ "nina", "check", "--show-trace" };405 const args = try parseArgs(gpa, &argv);406 defer gpa.free(args.rest);407 defer gpa.free(args.passthrough);408 try std.testing.expectEqualStrings("check", args.command);409 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);410 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);411}412413test "parse args forwards flake check --all-systems" {414 const gpa = std.testing.allocator;415 const argv = [_][:0]const u8{ "nina", "flake", "check", "--all-systems" };416 const args = try parseArgs(gpa, &argv);417 defer gpa.free(args.rest);418 defer gpa.free(args.passthrough);419 try std.testing.expectEqualStrings("flake", args.command);420 try std.testing.expectEqualStrings("check", args.sub);421 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);422 try std.testing.expectEqualStrings("--all-systems", args.passthrough[0]);423}424425test "parse args forwards build --print-build-logs" {426 const gpa = std.testing.allocator;427 const argv = [_][:0]const u8{ "nina", "build", "--print-build-logs" };428 const args = try parseArgs(gpa, &argv);429 defer gpa.free(args.rest);430 defer gpa.free(args.passthrough);431 try std.testing.expectEqualStrings("build", args.command);432 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);433 try std.testing.expectEqualStrings("--print-build-logs", args.passthrough[0]);434}435436test "parse args forwards develop --show-trace" {437 const gpa = std.testing.allocator;438 const argv = [_][:0]const u8{ "nina", "develop", "--show-trace" };439 const args = try parseArgs(gpa, &argv);440 defer gpa.free(args.rest);441 defer gpa.free(args.passthrough);442 try std.testing.expectEqualStrings("develop", args.command);443 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);444 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);445}446447test "parse args forwards run's -- separator and program args" {448 const gpa = std.testing.allocator;449 const argv = [_][:0]const u8{ "nina", "run", "hello", "--", "--version" };450 const args = try parseArgs(gpa, &argv);451 defer gpa.free(args.rest);452 defer gpa.free(args.passthrough);453 try std.testing.expectEqualStrings("run", args.command);454 try std.testing.expectEqual(@as(usize, 1), args.rest.len);455 try std.testing.expectEqualStrings("hello", args.rest[0]);456 try std.testing.expectEqual(@as(usize, 2), args.passthrough.len);457 try std.testing.expectEqualStrings("--", args.passthrough[0]);458 try std.testing.expectEqualStrings("--version", args.passthrough[1]);459}460461test "parse args forwards home apply --show-trace" {462 const gpa = std.testing.allocator;463 const argv = [_][:0]const u8{ "nina", "home", "apply", "--show-trace" };464 const args = try parseArgs(gpa, &argv);465 defer gpa.free(args.rest);466 defer gpa.free(args.passthrough);467 try std.testing.expectEqualStrings("home", args.command);468 try std.testing.expectEqualStrings("apply", args.sub);469 try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);470 try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);471}472473test "parse subcommand" {474 const gpa = std.testing.allocator;475 const argv = [_][:0]const u8{ "nina", "service", "list" };476 const args = try parseArgs(gpa, &argv);477 defer gpa.free(args.rest);478 try std.testing.expectEqualStrings("service", args.command);479 try std.testing.expectEqualStrings("list", args.sub);480}481482test "parse version flag" {483 const gpa = std.testing.allocator;484 const argv = [_][:0]const u8{ "nina", "--version" };485 const args = try parseArgs(gpa, &argv);486 defer gpa.free(args.rest);487 try std.testing.expect(args.version);488}