main.zig

13.1 KB · raw

1// main.zig — entry point, arg parsing, dispatch, error handler2// THE ONLY FILE that creates the Io.Writer and passes it to output.init().3const 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");1112const Args = struct {13    command: []const u8 = "",14    sub: []const u8 = "",15    rest: []const []const u8 = &.{},16    machine: ?[]const u8 = null,17    version: bool = false,18    dry: bool = false,19    check: bool = false,20    no_apply: bool = false,21    all: bool = false,22    json: bool = false,23    last: u32 = 50,24};2526pub fn main(startup: std.process.Init) !void {27    const arena = startup.arena.allocator();28    const gpa = startup.gpa;29    const io = startup.io;3031    // Command allocations go through an arena freed when main returns. nina is a32    // one-shot CLI that exits after one command, and std.process.exit skips33    // defers, so per-allocation frees were easy to miss — the arena guarantees34    // everything is released in one shot, eliminating exit-time leak reports.35    var cmd_arena = std.heap.ArenaAllocator.init(gpa);36    defer cmd_arena.deinit();37    const ca = cmd_arena.allocator();3839    // Initialize output module with buffered stdout writer. Streaming (not the40    // default positional) mode: stdout may be a terminal, pipe, or a redirected41    // regular file. Positional mode pwrites from offset 0 and ignores the fd's42    // inherited offset, which corrupts `{ echo h; nina x; } > f` and `>>` append43    // by overwriting from the start. Streaming honors the current file offset.44    var stdout_buf: [16384]u8 = undefined;45    var stdout_fw: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buf);46    output.init(&stdout_fw.interface, io, startup.environ_map, true);47    defer output.flush();4849    const argv = try startup.minimal.args.toSlice(arena);50    const args = try parseArgs(arena, argv);5152    if (args.version) {53        output.version();54        return;55    }5657    // Help and hello work without config58    if (args.command.len == 0 or std.mem.eql(u8, args.command, "help")) {59        output.help();60        return;61    }6263    // setup and update run before config so they work without ~/.nina.conf64    if (std.mem.eql(u8, args.command, "setup") or std.mem.eql(u8, args.command, "update")) {65        const dummy_machine: types.Machine = .{ .name = "local", .local = true };66        const dummy_cfg: types.NinaConfig = .{};67        const pre_ctx = commands.Ctx{68            .gpa = ca,69            .io = io,70            .environ = startup.environ_map,71            .machine = dummy_machine,72            .cfg = dummy_cfg,73            .args = args.rest,74            .sub = args.sub,75            .dry = args.dry,76            .check = args.check,77            .no_apply = args.no_apply,78            .all = args.all,79            .last = args.last,80            .json = args.json,81        };82        if (std.mem.eql(u8, args.command, "setup")) {83            commands.setup(ca, io, startup.environ_map) catch |err| {84                const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);85                const detail = errors.error_info.getDetail();86                const suggestion = errors.error_info.getSuggestion();87                output.printError(msg, detail, suggestion);88                output.flush();89                std.process.exit(1);90            };91        } else {92            dispatch(pre_ctx, args.command) catch |err| {93                reportError(err);94                output.flush();95                std.process.exit(1);96            };97        }98        return;99    }100101    // Load config and resolve machine. On first run with no config file, offer102    // the setup wizard when stdout is a TTY; skip silently in piped/scripted mode.103    const cfg = cfg_blk: {104        if (config.load(arena, io, startup.environ_map)) |c| {105            break :cfg_blk c;106        } else |err| {107            if (err == error.ConfigNotFound and (std.Io.File.stdout().isTty(io) catch false)) {108                commands.firstRunWizard(ca, io, startup.environ_map) catch |wizard_err| {109                    const msg = errors.error_info.getMessage() orelse errors.staticMessage(wizard_err);110                    const detail = errors.error_info.getDetail();111                    const suggestion = errors.error_info.getSuggestion();112                    output.printError(msg, detail, suggestion);113                    output.flush();114                    std.process.exit(1);115                };116                errors.error_info.reset();117                break :cfg_blk config.load(arena, io, startup.environ_map) catch |err2| {118                    const msg = errors.error_info.getMessage() orelse errors.staticMessage(err2);119                    const detail = errors.error_info.getDetail();120                    const suggestion = errors.error_info.getSuggestion();121                    output.printError(msg, detail, suggestion);122                    output.flush();123                    std.process.exit(1);124                };125            }126            const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);127            const detail = errors.error_info.getDetail();128            const suggestion = errors.error_info.getSuggestion();129            output.printError(msg, detail, suggestion);130            output.flush();131            std.process.exit(1);132        }133    };134135    output.setTeach(cfg.teach);136137    const machine = config.resolveMachine(&cfg, args.machine) catch |err| {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    };145146    const ctx = commands.Ctx{147        .gpa = ca,148        .io = io,149        .environ = startup.environ_map,150        .machine = machine,151        .cfg = cfg,152        .args = args.rest,153        .sub = args.sub,154        .dry = args.dry,155        .check = args.check,156        .no_apply = args.no_apply,157        .all = args.all,158        .last = args.last,159        .json = args.json,160    };161162    dispatch(ctx, args.command) catch |err| {163        reportError(err);164        output.flush();165        std.process.exit(1);166    };167}168169// Network/ssh/search failures are the machine's fault, not the user's — surface170// them with the sad kaomoji. Everything else (not found, bad config, bad args)171// is a plain error line.172fn reportError(err: anyerror) void {173    const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);174    const detail = errors.error_info.getDetail();175    const suggestion = errors.error_info.getSuggestion();176    switch (err) {177        error.NetworkError, error.SshFailed, error.SearchFailed => output.printSystemError(msg, detail, suggestion),178        else => output.printError(msg, detail, suggestion),179    }180}181182fn dispatch(ctx: commands.Ctx, cmd: []const u8) !void {183    if (std.mem.eql(u8, cmd, "apply")) return commands.apply(ctx);184    if (std.mem.eql(u8, cmd, "check")) return commands.check(ctx);185    if (std.mem.eql(u8, cmd, "back")) return commands.back(ctx);186    if (std.mem.eql(u8, cmd, "go")) return commands.go(ctx);187    if (std.mem.eql(u8, cmd, "history")) return commands.history(ctx);188    if (std.mem.eql(u8, cmd, "gen")) return commands.genCmd(ctx);189    if (std.mem.eql(u8, cmd, "clean")) return commands.clean(ctx);190    if (std.mem.eql(u8, cmd, "status")) return commands.statusCmd(ctx);191    if (std.mem.eql(u8, cmd, "diff")) return commands.diff(ctx);192    if (std.mem.eql(u8, cmd, "doctor")) return commands.doctor(ctx);193    if (std.mem.eql(u8, cmd, "mood")) return commands.mood(ctx);194    if (std.mem.eql(u8, cmd, "update")) return commands.update(ctx);195    if (std.mem.eql(u8, cmd, "upgrade")) return commands.upgrade(ctx);196    if (std.mem.eql(u8, cmd, "log")) return commands.log(ctx);197    if (std.mem.eql(u8, cmd, "edit")) return commands.edit(ctx);198    if (std.mem.eql(u8, cmd, "fmt")) return commands.fmt(ctx);199    if (std.mem.eql(u8, cmd, "repl")) return commands.repl(ctx);200    if (std.mem.eql(u8, cmd, "develop")) return commands.develop(ctx);201    if (std.mem.eql(u8, cmd, "info")) return commands.info(ctx);202    if (std.mem.eql(u8, cmd, "boot")) return commands.boot(ctx);203    if (std.mem.eql(u8, cmd, "search")) return commands.searchCmd(ctx);204    if (std.mem.eql(u8, cmd, "option")) return commands.optionCmd(ctx);205    if (std.mem.eql(u8, cmd, "install")) return commands.install(ctx);206    if (std.mem.eql(u8, cmd, "remove")) return commands.remove(ctx);207    if (std.mem.eql(u8, cmd, "list")) return commands.list(ctx);208    if (std.mem.eql(u8, cmd, "try")) return commands.tryCmd(ctx);209    if (std.mem.eql(u8, cmd, "service")) return commands.service(ctx);210    if (std.mem.eql(u8, cmd, "flake")) return commands.flake(ctx);211    if (std.mem.eql(u8, cmd, "store")) return commands.store(ctx);212    if (std.mem.eql(u8, cmd, "channel")) return commands.channel(ctx);213    if (std.mem.eql(u8, cmd, "profile")) return commands.profile(ctx);214    if (std.mem.eql(u8, cmd, "pkg")) return commands.pkg(ctx);215    if (std.mem.eql(u8, cmd, "pin")) return commands.pin(ctx);216    if (std.mem.eql(u8, cmd, "unpin")) return commands.unpin(ctx);217    if (std.mem.eql(u8, cmd, "hash")) return commands.hash(ctx);218    if (std.mem.eql(u8, cmd, "fetch")) return commands.fetch(ctx);219    if (std.mem.eql(u8, cmd, "build")) return commands.build(ctx);220    if (std.mem.eql(u8, cmd, "run")) return commands.runCmd(ctx);221    if (std.mem.eql(u8, cmd, "goodbye")) return commands.goodbye(ctx);222    if (std.mem.eql(u8, cmd, "hello")) return commands.hello(ctx);223    if (std.mem.eql(u8, cmd, "help")) { output.help(); return; }224225    errors.error_info.setMessage("unknown command: {s}", .{cmd});226    errors.error_info.setSuggestion("nina help", .{});227    return error.Internal;228}229230fn parseArgs(arena: std.mem.Allocator, argv: []const [:0]const u8) !Args {231    var result = Args{};232    var rest: std.ArrayList([]const u8) = .empty;233    var i: usize = 1; // skip program name234235    // First positional = command236    if (i < argv.len and argv[i][0] != '-') {237        result.command = argv[i];238        i += 1;239    }240241    // Second positional (if not a flag) = subcommand for multi-word commands242    if (i < argv.len and argv[i][0] != '-') {243        const multi_word = isMultiWord(result.command);244        if (multi_word) {245            result.sub = argv[i];246            i += 1;247        }248    }249250    while (i < argv.len) : (i += 1) {251        const a = argv[i];252        if (std.mem.eql(u8, a, "--on") or std.mem.eql(u8, a, "-on")) {253            i += 1;254            if (i < argv.len) result.machine = argv[i];255        } else if (std.mem.eql(u8, a, "--dry")) {256            result.dry = true;257        } else if (std.mem.eql(u8, a, "--check")) {258            result.check = true;259        } else if (std.mem.eql(u8, a, "--version")) {260            result.version = true;261        } else if (std.mem.eql(u8, a, "--no-apply")) {262            result.no_apply = true;263        } else if (std.mem.eql(u8, a, "--all")) {264            result.all = true;265        } else if (std.mem.eql(u8, a, "--json")) {266            result.json = true;267        } else if (std.mem.eql(u8, a, "--last")) {268            i += 1;269            if (i < argv.len) result.last = std.fmt.parseInt(u32, argv[i], 10) catch 50;270        } else if (std.mem.eql(u8, a, "-f")) {271            // -f flag for service logs follow272            try rest.append(arena, "-f");273        } else if (a.len > 0 and a[0] != '-') {274            try rest.append(arena, a);275        }276        // Ignore unknown flags silently277    }278279    result.rest = try rest.toOwnedSlice(arena);280    return result;281}282283fn isMultiWord(cmd: []const u8) bool {284    const multi = [_][]const u8{ "service", "flake", "store", "channel", "profile", "pkg", "gen", "develop", "edit" };285    for (multi) |m| {286        if (std.mem.eql(u8, cmd, m)) return true;287    }288    return false;289}290291test "parse args basic" {292    const gpa = std.testing.allocator;293    const argv = [_][:0]const u8{ "nina", "apply", "--dry" };294    const args = try parseArgs(gpa, &argv);295    defer gpa.free(args.rest);296    try std.testing.expectEqualStrings("apply", args.command);297    try std.testing.expect(args.dry);298}299300test "parse args with machine" {301    const gpa = std.testing.allocator;302    const argv = [_][:0]const u8{ "nina", "status", "--on", "build-server" };303    const args = try parseArgs(gpa, &argv);304    defer gpa.free(args.rest);305    try std.testing.expectEqualStrings("status", args.command);306    try std.testing.expectEqualStrings("build-server", args.machine.?);307}308309test "parse subcommand" {310    const gpa = std.testing.allocator;311    const argv = [_][:0]const u8{ "nina", "service", "list" };312    const args = try parseArgs(gpa, &argv);313    defer gpa.free(args.rest);314    try std.testing.expectEqualStrings("service", args.command);315    try std.testing.expectEqualStrings("list", args.sub);316}317318test "parse version flag" {319    const gpa = std.testing.allocator;320    const argv = [_][:0]const u8{ "nina", "--version" };321    const args = try parseArgs(gpa, &argv);322    defer gpa.free(args.rest);323    try std.testing.expect(args.version);324}