//! CLI over the core: for debugging, and for parity tests against the TypeScript //! reference. const std = @import("std"); const usage = @import("usage.zig"); const help_text = \\usage \\ providers supported providers info \\ advance ... advance feed state across rounds \\ [--now-ms N] \\ advance - the same, from one JSON envelope line on stdin: \\ {"provider","credential","responses":[round...],"nowMs"?} \\ claims account id and email from an OAuth token \\ ; fn writeTo(file: std.Io.File, io: std.Io, bytes: []const u8) !void { var buffer: [8192]u8 = undefined; var writer = file.writer(io, &buffer); try writer.interface.writeAll(bytes); try writer.flush(); } fn emit(init: std.process.Init, bytes: []const u8) !void { try writeTo(std.Io.File.stdout(), init.io, bytes); try writeTo(std.Io.File.stdout(), init.io, "\n"); } fn fail(init: std.process.Init, message: []const u8) !u8 { try writeTo(std.Io.File.stderr(), init.io, message); try writeTo(std.Io.File.stderr(), init.io, "\n\n"); try writeTo(std.Io.File.stderr(), init.io, help_text); return 1; } fn nowMs() i64 { var ts: std.posix.timespec = undefined; if (std.posix.errno(std.posix.system.clock_gettime(.REALTIME, &ts)) != .SUCCESS) return 0; return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000); } fn readFile(init: std.process.Init, path: []const u8, arena: std.mem.Allocator) ![]u8 { return std.Io.Dir.cwd().readFileAlloc(init.io, path, arena, std.Io.Limit.limited(8 * 1024 * 1024)); } /// Replays every round through one fresh state, printing one step per line: a host keeps the /// responses, and the process keeps nothing. fn advanceRounds( init: std.process.Init, arena: std.mem.Allocator, provider: []const u8, credential: []const u8, rounds: []const []const u8, now_ms: i64, ) !u8 { var state = usage.State.init(arena); defer state.deinit(); for (rounds) |responses| { var w = usage.Writer.init(arena); const outcome = usage.advance(arena, &state, provider, credential, responses, now_ms) catch |e| { var err_w = usage.Writer.init(arena); usage.writeStep(&err_w, .{ .err = @errorName(e) }) catch return 1; try emit(init, err_w.items()); return 0; }; try usage.writeStep(&w, outcome); try emit(init, w.items()); } return 0; } /// `advance -`: the whole call as one JSON envelope on stdin, so a credential never reaches argv, /// where any local process can read it. The envelope ends at the first newline, not only at EOF: /// a host that cannot half-close a pipe (an Erlang port) writes one line and reads the answer. fn advanceEnvelope(init: std.process.Init, arena: std.mem.Allocator) !u8 { const buffer = try arena.alloc(u8, 8 * 1024 * 1024); var reader = std.Io.File.stdin().readerStreaming(init.io, buffer); const input = (reader.interface.takeDelimiter('\n') catch |e| return fail(init, @errorName(e))) orelse return fail(init, "the envelope is empty"); const parsed = std.json.parseFromSliceLeaky(std.json.Value, arena, input, .{}) catch return fail(init, "the envelope is not JSON"); const root = usage.asObject(parsed) orelse return fail(init, "the envelope is not an object"); const provider = usage.asString(root.get("provider")) orelse return fail(init, "the envelope needs a provider"); const credential = root.get("credential") orelse return fail(init, "the envelope needs a credential"); const responses = usage.asArray(root.get("responses")) orelse return fail(init, "the envelope needs responses"); const now_ms = usage.toInt(i64, usage.asNumber(root.get("nowMs"))) orelse nowMs(); // An envelope asking for the first requests has no rounds yet; that is one empty round. var rounds = std.ArrayList([]const u8).empty; if (responses.items.len == 0) try rounds.append(arena, "[]"); for (responses.items) |round| { try rounds.append(arena, try std.json.Stringify.valueAlloc(arena, round, .{})); } const credential_text = try std.json.Stringify.valueAlloc(arena, credential, .{}); return advanceRounds(init, arena, provider, credential_text, rounds.items, now_ms); } pub fn main(init: std.process.Init) !u8 { const arena = init.arena.allocator(); const argv = try std.process.Args.toSlice(init.minimal.args, arena); if (argv.len < 2) return fail(init, "missing command"); const command = argv[1]; if (std.mem.eql(u8, command, "providers")) { var w = usage.Writer.init(arena); try usage.writeProviders(&w); try emit(init, w.items()); return 0; } if (std.mem.eql(u8, command, "advance")) { if (argv.len == 3 and std.mem.eql(u8, argv[2], "-")) return advanceEnvelope(init, arena); if (argv.len < 5) return fail(init, "advance needs ... or -"); const provider = argv[2]; const cred_arg = argv[3]; var resp_args = std.ArrayList([]const u8).empty; var now_ms = nowMs(); var i: usize = 4; while (i < argv.len) : (i += 1) { if (std.mem.eql(u8, argv[i], "--now-ms") and i + 1 < argv.len) { now_ms = std.fmt.parseInt(i64, argv[i + 1], 10) catch now_ms; i += 1; } else { try resp_args.append(arena, argv[i]); } } if (resp_args.items.len == 0) return fail(init, "advance needs at least one responses argument"); const cred_str = if (std.mem.startsWith(u8, cred_arg, "{")) cred_arg else readFile(init, cred_arg, arena) catch |e| return fail(init, @errorName(e)); var rounds = std.ArrayList([]const u8).empty; for (resp_args.items) |resp_arg| { const resp_str = if (std.mem.startsWith(u8, resp_arg, "[") or std.mem.startsWith(u8, resp_arg, "{")) resp_arg else readFile(init, resp_arg, arena) catch |e| return fail(init, @errorName(e)); try rounds.append(arena, resp_str); } return advanceRounds(init, arena, provider, cred_str, rounds.items, now_ms); } if (std.mem.eql(u8, command, "claims")) { if (argv.len < 3) return fail(init, "claims needs an access token"); var w = usage.Writer.init(arena); const outcome = try usage.claimsFromAccessToken(arena, argv[2], &w); switch (outcome) { .ok => { try emit(init, w.items()); return 0; }, .err => |text| return fail(init, text), } } return fail(init, "unknown command"); }