main.zig

14.7 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");11const man = @import("man.zig");1213const Args = struct {14    command: []const u8 = "",15    sub: []const u8 = "",16    rest: []const []const u8 = &.{},17    machine: ?[]const u8 = null,18    version: bool = false,19    dry: bool = false,20    check: bool = false,21    no_apply: bool = false,22    all: bool = false,23    json: bool = false,24    last: u32 = 50,25    user: bool = false,26};2728pub fn main(startup: std.process.Init) !void {29    const arena = startup.arena.allocator();30    const gpa = startup.gpa;31    const io = startup.io;3233    // Command allocations go through an arena freed when main returns. nina is a34    // one-shot CLI that exits after one command, and std.process.exit skips35    // defers, so per-allocation frees were easy to miss — the arena guarantees36    // everything is released in one shot, eliminating exit-time leak reports.37    var cmd_arena = std.heap.ArenaAllocator.init(gpa);38    defer cmd_arena.deinit();39    const ca = cmd_arena.allocator();4041    // Initialize output module with buffered stdout writer. Streaming (not the42    // default positional) mode: stdout may be a terminal, pipe, or a redirected43    // regular file. Positional mode pwrites from offset 0 and ignores the fd's44    // inherited offset, which corrupts `{ echo h; nina x; } > f` and `>>` append45    // by overwriting from the start. Streaming honors the current file offset.46    var stdout_buf: [16384]u8 = undefined;47    var stdout_fw: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buf);48    output.init(&stdout_fw.interface, io, startup.environ_map, true);49    defer output.flush();5051    const argv = try startup.minimal.args.toSlice(arena);52    const args = try parseArgs(arena, argv);5354    if (args.version) {55        output.version();56        return;57    }5859    // Help and hello work without config60    if (args.command.len == 0 or std.mem.eql(u8, args.command, "help")) {61        output.help();62        return;63    }6465    // man works without a config — it's pure docs, no machine context needed.66    if (std.mem.eql(u8, args.command, "man")) {67        const topic: []const u8 = if (args.rest.len > 0) args.rest[0] else "";68        man.run(io, topic) catch {};69        return;70    }7172    // setup and update run before config so they work without ~/.nina.conf73    if (std.mem.eql(u8, args.command, "setup") or std.mem.eql(u8, args.command, "update")) {74        const dummy_machine: types.Machine = .{ .name = "local", .local = true };75        const dummy_cfg: types.NinaConfig = .{};76        const pre_ctx = commands.Ctx{77            .gpa = ca,78            .io = io,79            .environ = startup.environ_map,80            .machine = dummy_machine,81            .cfg = dummy_cfg,82            .args = args.rest,83            .sub = args.sub,84            .dry = args.dry,85            .check = args.check,86            .no_apply = args.no_apply,87            .all = args.all,88            .last = args.last,89            .json = args.json,90            .user = args.user,91        };92        if (std.mem.eql(u8, args.command, "setup")) {93            commands.setup(ca, io, startup.environ_map) catch |err| {94                const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);95                const detail = errors.error_info.getDetail();96                const suggestion = errors.error_info.getSuggestion();97                output.printError(msg, detail, suggestion);98                output.flush();99                std.process.exit(1);100            };101        } else {102            dispatch(pre_ctx, args.command) catch |err| {103                reportError(err, ca);104                output.flush();105                std.process.exit(1);106            };107        }108        return;109    }110111    // Load config and resolve machine. On first run with no config file, offer112    // the setup wizard when stdout is a TTY; skip silently in piped/scripted mode.113    const cfg = cfg_blk: {114        if (config.load(arena, io, startup.environ_map)) |c| {115            break :cfg_blk c;116        } else |err| {117            if (err == error.ConfigNotFound and (std.Io.File.stdout().isTty(io) catch false)) {118                commands.firstRunWizard(ca, io, startup.environ_map) catch |wizard_err| {119                    const msg = errors.error_info.getMessage() orelse errors.staticMessage(wizard_err);120                    const detail = errors.error_info.getDetail();121                    const suggestion = errors.error_info.getSuggestion();122                    output.printError(msg, detail, suggestion);123                    output.flush();124                    std.process.exit(1);125                };126                errors.error_info.reset();127                break :cfg_blk config.load(arena, io, startup.environ_map) catch |err2| {128                    const msg = errors.error_info.getMessage() orelse errors.staticMessage(err2);129                    const detail = errors.error_info.getDetail();130                    const suggestion = errors.error_info.getSuggestion();131                    output.printError(msg, detail, suggestion);132                    output.flush();133                    std.process.exit(1);134                };135            }136            const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);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    };144145    output.setTeach(cfg.teach);146147    const machine = config.resolveMachine(&cfg, args.machine) catch |err| {148        const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);149        const detail = errors.error_info.getDetail();150        const suggestion = errors.error_info.getSuggestion();151        output.printError(msg, detail, suggestion);152        output.flush();153        std.process.exit(1);154    };155156    const ctx = commands.Ctx{157        .gpa = ca,158        .io = io,159        .environ = startup.environ_map,160        .machine = machine,161        .cfg = cfg,162        .args = args.rest,163        .sub = args.sub,164        .dry = args.dry,165        .check = args.check,166        .no_apply = args.no_apply,167        .all = args.all,168        .last = args.last,169        .json = args.json,170        .user = args.user,171    };172173    dispatch(ctx, args.command) catch |err| {174        reportError(err, ca);175        output.flush();176        std.process.exit(1);177    };178}179180// Network/ssh/search failures are the machine's fault, not the user's — surface181// them with the sad kaomoji. BuildFailed is routed through the 3-layer182// translation pipeline. Everything else uses a plain error line.183fn reportError(err: anyerror, allocator: std.mem.Allocator) void {184    switch (err) {185        // abort message already printed by output.aborted() — just exit non-zero186        error.HookAborted => {},187        error.BuildFailed => {188            const stderr = errors.getBuildStderr();189            const translated = errors.translateError(allocator, stderr);190            output.buildError(translated);191        },192        error.NetworkError, error.SshFailed, error.SearchFailed => {193            const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);194            const detail = errors.error_info.getDetail();195            const suggestion = errors.error_info.getSuggestion();196            output.printSystemError(msg, detail, suggestion);197        },198        else => {199            const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);200            const detail = errors.error_info.getDetail();201            const suggestion = errors.error_info.getSuggestion();202            output.printError(msg, detail, suggestion);203        },204    }205}206207fn dispatch(ctx: commands.Ctx, cmd: []const u8) !void {208    if (std.mem.eql(u8, cmd, "apply")) return commands.apply(ctx);209    if (std.mem.eql(u8, cmd, "check")) return commands.check(ctx);210    if (std.mem.eql(u8, cmd, "back")) return commands.back(ctx);211    if (std.mem.eql(u8, cmd, "go")) return commands.go(ctx);212    if (std.mem.eql(u8, cmd, "history")) return commands.history(ctx);213    if (std.mem.eql(u8, cmd, "gen")) return commands.genCmd(ctx);214    if (std.mem.eql(u8, cmd, "clean")) return commands.clean(ctx);215    if (std.mem.eql(u8, cmd, "status")) return commands.statusCmd(ctx);216    if (std.mem.eql(u8, cmd, "diff")) return commands.diff(ctx);217    if (std.mem.eql(u8, cmd, "doctor")) return commands.doctor(ctx);218    if (std.mem.eql(u8, cmd, "mood")) return commands.mood(ctx);219    if (std.mem.eql(u8, cmd, "update")) return commands.update(ctx);220    if (std.mem.eql(u8, cmd, "upgrade")) return commands.upgrade(ctx);221    if (std.mem.eql(u8, cmd, "log")) return commands.log(ctx);222    if (std.mem.eql(u8, cmd, "edit")) return commands.edit(ctx);223    if (std.mem.eql(u8, cmd, "fmt")) return commands.fmt(ctx);224    if (std.mem.eql(u8, cmd, "repl")) return commands.repl(ctx);225    if (std.mem.eql(u8, cmd, "develop")) return commands.develop(ctx);226    if (std.mem.eql(u8, cmd, "info")) return commands.info(ctx);227    if (std.mem.eql(u8, cmd, "boot")) return commands.boot(ctx);228    if (std.mem.eql(u8, cmd, "search")) return commands.searchCmd(ctx);229    if (std.mem.eql(u8, cmd, "option")) return commands.optionCmd(ctx);230    if (std.mem.eql(u8, cmd, "install")) return commands.install(ctx);231    if (std.mem.eql(u8, cmd, "remove")) return commands.remove(ctx);232    if (std.mem.eql(u8, cmd, "list")) return commands.list(ctx);233    if (std.mem.eql(u8, cmd, "try")) return commands.tryCmd(ctx);234    if (std.mem.eql(u8, cmd, "service")) return commands.service(ctx);235    if (std.mem.eql(u8, cmd, "flake")) return commands.flake(ctx);236    if (std.mem.eql(u8, cmd, "store")) return commands.store(ctx);237    if (std.mem.eql(u8, cmd, "channel")) return commands.channel(ctx);238    if (std.mem.eql(u8, cmd, "profile")) return commands.profile(ctx);239    if (std.mem.eql(u8, cmd, "pkg")) return commands.pkg(ctx);240    if (std.mem.eql(u8, cmd, "why")) return commands.why(ctx);241    if (std.mem.eql(u8, cmd, "pin")) return commands.pin(ctx);242    if (std.mem.eql(u8, cmd, "unpin")) return commands.unpin(ctx);243    if (std.mem.eql(u8, cmd, "hash")) return commands.hash(ctx);244    if (std.mem.eql(u8, cmd, "fetch")) return commands.fetch(ctx);245    if (std.mem.eql(u8, cmd, "build")) return commands.build(ctx);246    if (std.mem.eql(u8, cmd, "run")) return commands.runCmd(ctx);247    if (std.mem.eql(u8, cmd, "home")) return commands.home(ctx);248    if (std.mem.eql(u8, cmd, "goodbye")) return commands.goodbye(ctx);249    if (std.mem.eql(u8, cmd, "hello")) return commands.hello(ctx);250    if (std.mem.eql(u8, cmd, "help")) { output.help(); return; }251252    errors.error_info.setMessage("unknown command: {s}", .{cmd});253    errors.error_info.setSuggestion("nina help", .{});254    return error.Internal;255}256257fn parseArgs(arena: std.mem.Allocator, argv: []const [:0]const u8) !Args {258    var result = Args{};259    var rest: std.ArrayList([]const u8) = .empty;260    var i: usize = 1; // skip program name261262    // First positional = command263    if (i < argv.len and argv[i][0] != '-') {264        result.command = argv[i];265        i += 1;266    }267268    // Second positional (if not a flag) = subcommand for multi-word commands269    if (i < argv.len and argv[i][0] != '-') {270        const multi_word = isMultiWord(result.command);271        if (multi_word) {272            result.sub = argv[i];273            i += 1;274        }275    }276277    while (i < argv.len) : (i += 1) {278        const a = argv[i];279        if (std.mem.eql(u8, a, "--on") or std.mem.eql(u8, a, "-on")) {280            i += 1;281            if (i < argv.len) result.machine = argv[i];282        } else if (std.mem.eql(u8, a, "--dry")) {283            result.dry = true;284        } else if (std.mem.eql(u8, a, "--check")) {285            result.check = true;286        } else if (std.mem.eql(u8, a, "--version")) {287            result.version = true;288        } else if (std.mem.eql(u8, a, "--no-apply")) {289            result.no_apply = true;290        } else if (std.mem.eql(u8, a, "--all")) {291            result.all = true;292        } else if (std.mem.eql(u8, a, "--user")) {293            result.user = true;294        } else if (std.mem.eql(u8, a, "--json")) {295            result.json = true;296        } else if (std.mem.eql(u8, a, "--last")) {297            i += 1;298            if (i < argv.len) result.last = std.fmt.parseInt(u32, argv[i], 10) catch 50;299        } else if (std.mem.eql(u8, a, "-f")) {300            // -f flag for service logs follow301            try rest.append(arena, "-f");302        } else if (std.mem.eql(u8, a, "--switch")) {303            // --switch flag for `home init` (activate after init)304            try rest.append(arena, "--switch");305        } else if (std.mem.eql(u8, a, "--dir")) {306            // --dir flag for `nina edit` (open the config directory)307            try rest.append(arena, "--dir");308        } else if (a.len > 0 and a[0] != '-') {309            try rest.append(arena, a);310        }311        // Ignore unknown flags silently312    }313314    result.rest = try rest.toOwnedSlice(arena);315    return result;316}317318fn isMultiWord(cmd: []const u8) bool {319    const multi = [_][]const u8{ "service", "flake", "store", "channel", "profile", "pkg", "gen", "develop", "edit", "home" };320    for (multi) |m| {321        if (std.mem.eql(u8, cmd, m)) return true;322    }323    return false;324}325326test "parse args basic" {327    const gpa = std.testing.allocator;328    const argv = [_][:0]const u8{ "nina", "apply", "--dry" };329    const args = try parseArgs(gpa, &argv);330    defer gpa.free(args.rest);331    try std.testing.expectEqualStrings("apply", args.command);332    try std.testing.expect(args.dry);333}334335test "parse args with machine" {336    const gpa = std.testing.allocator;337    const argv = [_][:0]const u8{ "nina", "status", "--on", "build-server" };338    const args = try parseArgs(gpa, &argv);339    defer gpa.free(args.rest);340    try std.testing.expectEqualStrings("status", args.command);341    try std.testing.expectEqualStrings("build-server", args.machine.?);342}343344test "parse subcommand" {345    const gpa = std.testing.allocator;346    const argv = [_][:0]const u8{ "nina", "service", "list" };347    const args = try parseArgs(gpa, &argv);348    defer gpa.free(args.rest);349    try std.testing.expectEqualStrings("service", args.command);350    try std.testing.expectEqualStrings("list", args.sub);351}352353test "parse version flag" {354    const gpa = std.testing.allocator;355    const argv = [_][:0]const u8{ "nina", "--version" };356    const args = try parseArgs(gpa, &argv);357    defer gpa.free(args.rest);358    try std.testing.expect(args.version);359}