//! Subcommand dispatch for the `panto` CLI. //! //! Routes argv[1] to one of: //! - `lua` — drop into the embedded standalone Lua interpreter //! (panto's `lua.c` build), with luarocks's runtime //! bootstrap completed first so `require("luarocks.*")` //! and the configured rocks tree work the same as in //! the agent process. //! - `bootstrap` — run the luarocks runtime bootstrap pipeline only; //! exit before entering any agent loop. Lets users //! do first-run setup on a fresh machine without //! starting a chat session. //! - anything else (or absent) — fall through to the agent REPL. //! //! Both `lua` and `bootstrap` end up calling `luarocks_runtime.bootstrap` //! before doing their thing. The agent path does the same; the only //! difference is whether the agent loop runs afterward. const std = @import("std"); const Allocator = std.mem.Allocator; const Io = std.Io; const lua_bridge = @import("lua_bridge.zig"); const luarocks_runtime = @import("luarocks_runtime.zig"); const self_exe = @import("self_exe.zig"); const session_paths = @import("session_paths.zig"); const config_file = @import("config_file.zig"); const models_toml = @import("models_toml.zig"); const panto_home = @import("panto_home.zig"); const panto = @import("panto"); const c = lua_bridge.c; pub const Action = enum { /// Continue with the default agent REPL. agent, /// Bootstrap is already done; the dispatcher consumed the subcommand. /// `main` should exit immediately. done, }; /// Inspect `argv[1]`, run the appropriate subcommand, and return what /// the caller should do next. On `.agent`, the dispatcher leaves argv /// untouched and `main` continues as before. On `.done`, the caller /// must return promptly (the subcommand has already produced output). /// /// `panto_executable_path` is the absolute path of the running panto /// binary, used both to wire up the embedded luarocks `LUA` variable /// and to `exec` ourselves where needed. pub fn dispatch( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, args: std.process.Args, panto_executable_path: []const u8, ) !Action { var it = args.iterate(); defer it.deinit(); _ = it.next(); // argv[0] const sub = it.next() orelse return .agent; if (std.mem.eql(u8, sub, "lua")) { try runLuaSubcommand(allocator, io, environ_map, args, panto_executable_path); return .done; } if (std.mem.eql(u8, sub, "bootstrap")) { var force = false; while (it.next()) |flag| { if (std.mem.eql(u8, flag, "--force")) { force = true; } else { std.log.err("panto bootstrap: unknown flag '{s}'", .{flag}); return error.UnknownFlag; } } try runBootstrapSubcommand(allocator, io, environ_map, panto_executable_path, .{ .force = force }); return .done; } if (std.mem.eql(u8, sub, "sessions")) { try runSessionsSubcommand(allocator, io, environ_map); return .done; } if (std.mem.eql(u8, sub, "models")) { try runModelsSubcommand(allocator, io, environ_map, &it); return .done; } if (std.mem.eql(u8, sub, "auth")) { try runAuthSubcommand(allocator, io, environ_map, &it); return .done; } if (std.mem.eql(u8, sub, "--help") or std.mem.eql(u8, sub, "-h") or std.mem.eql(u8, sub, "help")) { try printHelp(io); return .done; } return .agent; } fn printHelp(io: Io) !void { var buffer: [4096]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &buffer); const w = &stdout_file.interface; try w.writeAll( \\panto — a conversational coding agent \\ \\Usage: \\ panto Start a new conversation. \\ panto --resume Resume the most recent conversation in this directory. \\ panto --resume Resume the conversation whose id begins with . \\ panto sessions List saved sessions for this directory. \\ panto models sync Fetch models.dev and rebuild the base models.toml. \\ panto auth status Show configured auth sessions and login state. \\ panto auth login \\ Log in to an OAuth auth session (device flow). \\ panto auth logout \\ Forget a stored OAuth token. \\ panto bootstrap [--force] \\ Run the luarocks bootstrap and exit. \\ panto lua [args...] Drop into the embedded Lua interpreter. \\ panto help Show this message. \\ \\Configuration (TOML, merged base → user → project): \\ $XDG_DATA_HOME/panto/config.toml (base; auto-generated) \\ $XDG_CONFIG_HOME/panto/config.toml (user) \\ ./.panto/config.toml (project) \\ Define providers under [providers.], pick a default with \\ [defaults] model = ":", and gate tools/extensions \\ with [tools]/[extensions] allow/deny globs. Model aliases (wire name, \\ reasoning, max_tokens, pricing) live in models.toml. \\ \\Environment: \\ OPENAI_API_KEY, ANTHROPIC_API_KEY Consumed by the default providers. \\ PANTO_SESSION_DIR Override the base sessions directory. Defaults to \\ $XDG_DATA_HOME/panto/sessions or ~/.local/share/panto/sessions. \\ PANTO_HOME Override the runtime/rocks tree location. \\ ); try stdout_file.flush(); } pub const BootstrapOptions = struct { /// Wipe the per-Lua-version tree before reinstalling everything. /// Surfaced as `panto bootstrap --force`. Equivalent to deleting /// `$PANTO_HOME/rocks/lua-X.Y.Z/` by hand and then running /// `panto bootstrap`. force: bool = false, }; // --------------------------------------------------------------------------- // `panto lua` // --------------------------------------------------------------------------- extern "c" fn panto_lua_pmain(L: *c.lua_State, argc: c_int, argv: [*]?[*:0]u8) c_int; /// Drop into the embedded Lua standalone interpreter, with the /// luarocks runtime bootstrap completed so `require("luarocks.*")` /// and rocks installed under `$PANTO_HOME` are visible. /// /// argv is rewritten so the interpreter sees `lua [...args]` rather /// than `panto lua [...args]` — matching upstream behavior. The first /// argument visible to `pmain` is the program name; this matters for /// `arg[0]` and error reporting. fn runLuaSubcommand( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, args: std.process.Args, panto_executable_path: []const u8, ) !void { // Build a fresh lua_State that we own, configure it like luarocks // expects, then hand it to `pmain`. const L = c.luaL_newstate() orelse return error.LuaInitFailed; defer c.lua_close(L); // Run bootstrap against this state. This installs the embedded // searcher, configures package.path/cpath, and stages on-disk // resources. We deliberately do NOT call `luaL_openlibs` here — // `pmain` does that itself, and we want exactly the upstream // ordering for everything that runs inside the REPL. // // The searcher install only requires `package.searchers` to be // present; the stock libs ship it. We open libs once here just // to satisfy that, then pmain's own `luaL_openlibs` is idempotent. c.luaL_openlibs(L); const rt = try luarocks_runtime.bootstrap( allocator, io, environ_map, L, panto_executable_path, ); defer rt.deinit(); // Re-create the argv the standalone interpreter expects. argv[0] // is the program name; argv[1..] are the user's args. var raw_args = args.iterate(); defer raw_args.deinit(); _ = raw_args.next(); // panto _ = raw_args.next(); // lua var argv_list: std.array_list.Managed([:0]u8) = .init(allocator); defer { for (argv_list.items) |s| allocator.free(s); argv_list.deinit(); } // Program name first. try argv_list.append(try allocator.dupeZ(u8, "lua")); while (raw_args.next()) |a| { try argv_list.append(try allocator.dupeZ(u8, a)); } // Build a `[*]?[*:0]u8` argv pointer array. lua.c expects a // NULL-terminated array (it uses `argv[i]` indexed access through // argc; the trailing NULL is conventional for C `main`). var argv_c: std.array_list.Managed(?[*:0]u8) = .init(allocator); defer argv_c.deinit(); for (argv_list.items) |s| { try argv_c.append(s.ptr); } try argv_c.append(null); const exit_code = panto_lua_pmain(L, @intCast(argv_list.items.len), argv_c.items.ptr); if (exit_code != 0) std.process.exit(@intCast(exit_code)); } // --------------------------------------------------------------------------- // `panto bootstrap` // --------------------------------------------------------------------------- /// Run the luarocks bootstrap and exit. Useful for first-run setup on /// a clean machine (downloads + compiles batteries, stages headers, /// materializes config) and for CI/scripted installs. /// /// Idempotent: subsequent invocations no-op fast, unless `force` was /// passed — then the entire per-Lua-version tree is wiped before the /// regular bootstrap pipeline runs. fn runBootstrapSubcommand( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, panto_executable_path: []const u8, opts: BootstrapOptions, ) !void { if (opts.force) { try luarocks_runtime.wipeTree(allocator, io, environ_map); } const L = c.luaL_newstate() orelse return error.LuaInitFailed; defer c.lua_close(L); c.luaL_openlibs(L); const rt = try luarocks_runtime.bootstrap( allocator, io, environ_map, L, panto_executable_path, ); defer rt.deinit(); // Pleasant single-line confirmation. The interesting bits (rock // installs etc.) print their own progress. std.log.info( "panto bootstrap: tree ready at {s}", .{rt.layout.tree}, ); } // --------------------------------------------------------------------------- // `panto sessions` // --------------------------------------------------------------------------- /// List sessions for the current working directory. /// /// Output format (one session per line): /// messages /// /// where `` is the first 8 hex chars of the session UUIDv7. fn runSessionsSubcommand( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, ) !void { var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; const cwd_n = try std.process.currentPath(io, &cwd_buf); const cwd = cwd_buf[0..cwd_n]; const session_dir = try session_paths.sessionDirForCwd(allocator, environ_map, cwd); defer allocator.free(session_dir); var store_impl = try panto.FileSystemJSONLStore.init(allocator, io, session_dir); defer store_impl.deinit(); const store = store_impl.store(); const infos = try store.list(); defer store.freeSessionInfos(infos); var stdout_buffer: [4096]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_file.interface; if (infos.len == 0) { try stdout.print("no sessions for {s}\n", .{cwd}); try stdout_file.flush(); return; } for (infos) |info| { const short = info.id[0..@min(8, info.id.len)]; // `created` is ISO 8601 (e.g. `2026-04-25T17:40:15.990Z`). Trim // to `YYYY-MM-DD HH:MM` for terseness. const created_short = trimCreated(info.created); try stdout.print( "{s} {s} {d} messages\n", .{ short, created_short, info.message_count }, ); } try stdout_file.flush(); } fn trimCreated(iso: []const u8) []const u8 { if (iso.len < 16) return iso; // `YYYY-MM-DDTHH:MM:...` → `YYYY-MM-DD HH:MM` (T → space). // We can't mutate a borrowed slice, so just return a 16-byte slice // of the original; the caller prints character-by-character via // format, so the 'T' will still appear. Use a small buffer trick: // return the slice unmodified — the 'T' is fine and unambiguous. return iso[0..16]; } // --------------------------------------------------------------------------- // `panto models` // --------------------------------------------------------------------------- const ModelsSyncResult = struct { content: []u8, providers_synced: usize, providers_skipped: usize, models_written: usize, }; /// `panto models sync` — fetch models.dev and rebuild the base-layer /// `models.toml` from the configured providers only. fn runModelsSubcommand( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, it: *std.process.Args.Iterator, ) !void { var out_buf: [4096]u8 = undefined; var out_file = std.Io.File.stdout().writer(io, &out_buf); const out = &out_file.interface; const action = it.next() orelse { try out.writeAll("usage: panto models sync\n"); try out_file.flush(); return; }; if (!std.mem.eql(u8, action, "sync")) { try out.print("panto models: unknown action '{s}'\nusage: panto models sync\n", .{action}); try out_file.flush(); return; } if (it.next()) |extra| { try out.print("panto models sync: unexpected argument '{s}'\n", .{extra}); try out_file.flush(); return; } var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; const cwd_n = try std.process.currentPath(io, &cwd_buf); const cwd = cwd_buf[0..cwd_n]; var cfg = config_file.load(allocator, io, environ_map, cwd) catch |err| { std.log.err("panto models sync: failed to load config ({t})", .{err}); return err; }; defer cfg.deinit(); const catalog_json = try fetchModelsDevCatalog(allocator, io); defer allocator.free(catalog_json); const sync = try buildSyncedModelsToml(allocator, &cfg, catalog_json); defer allocator.free(sync.content); const base_models_path = try models_toml.basePath(allocator, environ_map); defer allocator.free(base_models_path); if (std.fs.path.dirname(base_models_path)) |parent| { Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } try Io.Dir.cwd().writeFile(io, .{ .sub_path = base_models_path, .data = sync.content }); try out.print( "synced {d} model(s) across {d} provider(s) to {s}\n", .{ sync.models_written, sync.providers_synced, base_models_path }, ); if (sync.providers_skipped > 0) { try out.print( "{d} configured provider(s) had no matching catalog entry and were skipped\n", .{sync.providers_skipped}, ); } try out_file.flush(); } fn fetchModelsDevCatalog(allocator: Allocator, io: Io) ![]u8 { panto.init(allocator, io); defer panto.deinit(); const client = panto.httpClient(); var body: std.Io.Writer.Allocating = .init(allocator); defer body.deinit(); const headers = [_]std.http.Header{.{ .name = "User-Agent", .value = "panto models sync" }}; const res = try client.fetch(.{ .location = .{ .url = "https://models.dev/api.json" }, .extra_headers = &headers, .response_writer = &body.writer, }); if (res.status != .ok) return error.ModelCatalogFetchFailed; return try body.toOwnedSlice(); } fn buildSyncedModelsToml( allocator: Allocator, cfg: *const config_file.Config, catalog_json: []const u8, ) !ModelsSyncResult { var parsed = try std.json.parseFromSlice(std.json.Value, allocator, catalog_json, .{}); defer parsed.deinit(); const root = switch (parsed.value) { .object => |obj| obj, else => return error.InvalidModelCatalog, }; var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); try out.writer.writeAll( "# panto base models config (generated by `panto models sync`).\n" ++ "#\n" ++ "# This is the lowest-precedence models layer. Override anything here in:\n" ++ "# ~/.config/panto/models.toml\n" ++ "# ./.panto/models.toml\n" ++ "# ./.panto/models.local.toml\n" ++ "#\n" ++ "# Generated from https://models.dev/api.json for the providers currently\n" ++ "# configured in panto. Safe to overwrite by running the sync again.\n\n", ); var providers_synced: usize = 0; var providers_skipped: usize = 0; var models_written: usize = 0; for (cfg.providers) |p| { const catalog_name = p.model_catalog_name orelse p.name; const provider_v = root.get(catalog_name) orelse { providers_skipped += 1; continue; }; const provider_obj = switch (provider_v) { .object => |obj| obj, else => { providers_skipped += 1; continue; }, }; const models_v = provider_obj.get("models") orelse { providers_skipped += 1; continue; }; const models_obj = switch (models_v) { .object => |obj| obj, else => { providers_skipped += 1; continue; }, }; var wrote_provider = false; var it_models = models_obj.iterator(); while (it_models.next()) |kv| { const model_obj = switch (kv.value_ptr.*) { .object => |obj| obj, else => continue, }; const model_id = if (model_obj.get("id")) |id_v| switch (id_v) { .string => |s| s, else => kv.key_ptr.*, } else kv.key_ptr.*; if (!wrote_provider) { wrote_provider = true; providers_synced += 1; try out.writer.print("# provider {s} (catalog: {s})\n", .{ p.name, catalog_name }); } try writeTomlTableHeader(&out.writer, p.name, model_id); if (model_obj.get("limit")) |limit_v| switch (limit_v) { .object => |limit_obj| { if (limit_obj.get("context")) |context_v| { if (jsonPositiveU32(context_v)) |n| { try out.writer.print("context_window = {d}\n", .{n}); } } if (limit_obj.get("output")) |output_v| { if (jsonPositiveU32(output_v)) |n| { try out.writer.print("max_tokens = {d}\n", .{n}); } } }, else => {}, }; if (model_obj.get("cost")) |cost_v| switch (cost_v) { .object => |cost_obj| { try writeTomlPriceField(&out.writer, "input", cost_obj.get("input")); try writeTomlPriceField(&out.writer, "output", cost_obj.get("output")); try writeTomlPriceField(&out.writer, "cache_read", cost_obj.get("cache_read")); try writeTomlPriceField(&out.writer, "cache_write", cost_obj.get("cache_write")); }, else => {}, }; try out.writer.writeByte('\n'); models_written += 1; } if (wrote_provider) { try out.writer.writeByte('\n'); } else { providers_skipped += 1; } } return .{ .content = try out.toOwnedSlice(), .providers_synced = providers_synced, .providers_skipped = providers_skipped, .models_written = models_written, }; } fn writeTomlTableHeader(w: *std.Io.Writer, provider: []const u8, alias: []const u8) !void { try w.writeByte('['); try writeTomlBasicString(w, provider); try w.writeByte('.'); try writeTomlBasicString(w, alias); try w.writeAll("]\n"); } fn writeTomlBasicString(w: *std.Io.Writer, s: []const u8) !void { try w.writeByte('"'); for (s) |ch| switch (ch) { '\\' => try w.writeAll("\\\\"), '"' => try w.writeAll("\\\""), '\n' => try w.writeAll("\\n"), '\r' => try w.writeAll("\\r"), '\t' => try w.writeAll("\\t"), else => try w.writeByte(ch), }; try w.writeByte('"'); } fn jsonPositiveU32(v: std.json.Value) ?u32 { return switch (v) { .integer => |i| if (i > 0 and i <= std.math.maxInt(u32)) @intCast(i) else null, .float => |f| if (std.math.isFinite(f) and f > 0 and f <= @as(f64, @floatFromInt(std.math.maxInt(u32)))) @intFromFloat(f) else null, .number_string => |s| std.fmt.parseInt(u32, s, 10) catch null, else => null, }; } fn jsonNonNegativeNumber(v: std.json.Value) ?f64 { return switch (v) { .integer => |i| if (i >= 0) @floatFromInt(i) else null, .float => |f| if (std.math.isFinite(f) and f >= 0) f else null, .number_string => |s| blk: { const f = std.fmt.parseFloat(f64, s) catch break :blk null; break :blk if (std.math.isFinite(f) and f >= 0) f else null; }, else => null, }; } fn writeTomlPriceField(w: *std.Io.Writer, name: []const u8, v: ?std.json.Value) !void { const raw = v orelse return; const n = jsonNonNegativeNumber(raw) orelse return; if (@round(n) == n) { try w.print("{s} = {d}\n", .{ name, @as(i64, @intFromFloat(n)) }); } else { try w.print("{s} = {d}\n", .{ name, n }); } } // --------------------------------------------------------------------------- // `panto auth` // --------------------------------------------------------------------------- /// Line-based device-code presenter for the `panto auth login` flow (the TUI /// is not running here). Prints the verification URL + user code to stdout. const CliPresenter = struct { io: Io, fn deviceCode(ptr: *anyopaque, prompt: panto.DeviceCodePrompt) void { const self: *CliPresenter = @ptrCast(@alignCast(ptr)); var buf: [1024]u8 = undefined; var fw = std.Io.File.stdout().writer(self.io, &buf); const w = &fw.interface; w.print( "\nTo authorize, open this URL in a browser:\n {s}\n\nand enter the code:\n {s}\n\n", .{ prompt.verification_uri, prompt.user_code }, ) catch {}; fw.flush() catch {}; } fn status(ptr: *anyopaque, msg: []const u8) void { const self: *CliPresenter = @ptrCast(@alignCast(ptr)); var buf: [256]u8 = undefined; var fw = std.Io.File.stdout().writer(self.io, &buf); const w = &fw.interface; w.print("{s}\n", .{msg}) catch {}; fw.flush() catch {}; } const vtable: panto.Presenter.VTable = .{ .on_device_code = deviceCode, .on_status = status, }; fn presenter(self: *CliPresenter) panto.Presenter { return .{ .ptr = self, .vtable = &vtable }; } }; fn nowUnix(io: Io) i64 { const ns = std.Io.Clock.now(.real, io).nanoseconds; return @intCast(@divFloor(ns, std.time.ns_per_s)); } /// `panto auth [status|login |logout ]`. fn runAuthSubcommand( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, it: *std.process.Args.Iterator, ) !void { var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; const cwd_n = try std.process.currentPath(io, &cwd_buf); const cwd = cwd_buf[0..cwd_n]; var cfg = config_file.load(allocator, io, environ_map, cwd) catch |err| { std.log.err("panto auth: failed to load config ({t})", .{err}); return err; }; defer cfg.deinit(); var layout = try panto_home.resolve(allocator, environ_map); defer layout.deinit(); const auth_dir = layout.auth_dir; var out_buf: [4096]u8 = undefined; var out_file = std.Io.File.stdout().writer(io, &out_buf); const out = &out_file.interface; const action = it.next() orelse "status"; if (std.mem.eql(u8, action, "status")) { try authStatus(allocator, io, &cfg, auth_dir, out); try out_file.flush(); return; } if (std.mem.eql(u8, action, "logout")) { const name = it.next() orelse { try out.writeAll("usage: panto auth logout \n"); try out_file.flush(); return; }; const removed = try panto.deleteTokenSet(allocator, io, auth_dir, name); if (removed) { try out.print("logged out of '{s}'\n", .{name}); } else { try out.print("no stored token for '{s}'\n", .{name}); } try out_file.flush(); return; } if (std.mem.eql(u8, action, "login")) { const name = it.next() orelse { try out.writeAll("usage: panto auth login \n"); try out_file.flush(); return; }; try authLogin(allocator, io, &cfg, auth_dir, name, out); try out_file.flush(); return; } try out.print("unknown auth action '{s}' (try: status, login, logout)\n", .{action}); try out_file.flush(); } fn authStatus( allocator: Allocator, io: Io, cfg: *const config_file.Config, auth_dir: []const u8, out: *std.Io.Writer, ) !void { if (cfg.auths.len == 0) { try out.writeAll("no auth sessions configured\n"); return; } const now = nowUnix(io); for (cfg.auths) |a| { switch (a.config) { .api_key => { const state = if (a.resolved_api_key != null) "resolved" else "unresolved (key/env missing)"; try out.print("{s} api_key {s}\n", .{ a.name, state }); }, .oauth_device => { var loaded = panto.loadTokenSet(allocator, io, auth_dir, a.name) catch null; defer if (loaded) |*l| l.deinit(); if (loaded) |l| { const ts = l.value; if (ts.expires_at) |exp| { const mins = @divFloor(exp - now, 60); try out.print("{s} oauth_device logged in (access expires in ~{d}m)\n", .{ a.name, mins }); } else { try out.print("{s} oauth_device logged in\n", .{a.name}); } } else { try out.print("{s} oauth_device not logged in (run: panto auth login {s})\n", .{ a.name, a.name }); } }, } } } fn authLogin( allocator: Allocator, io: Io, cfg: *const config_file.Config, auth_dir: []const u8, name: []const u8, out: *std.Io.Writer, ) !void { const a = cfg.auth(name) orelse { try out.print("no auth session named '{s}' in config\n", .{name}); return; }; const oauth = switch (a.config) { .oauth_device => |o| o, .api_key => { try out.print("'{s}' is an api_key session; nothing to log in to\n", .{name}); return; }, }; panto.init(allocator, io); defer panto.deinit(); const client = panto.httpClient(); // The auth HTTP calls carry the identity headers of a provider that uses // this session (e.g. Copilot's editor headers). 1:1 in practice; if no // provider references it yet, send none. const headers: []const panto.Header = blk: { for (cfg.providers) |p| { if (std.mem.eql(u8, p.auth_name, name)) break :blk p.extra_headers; } break :blk &.{}; }; var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const aa = arena.allocator(); var presenter = CliPresenter{ .io = io }; const toks = panto.oauthLogin(aa, io, client, oauth, presenter.presenter(), headers) catch |err| { try out.print("login failed: {t}\n", .{err}); return; }; const now = nowUnix(io); var ts = try panto.tokensToTokenSet(aa, oauth, toks, now); // Run the secondary exchange now (if configured) so the first turn is // immediately usable and we surface any exchange error during login. if (oauth.exchange) |exchange| { if (ts.access_token) |access| { ts.exchange = panto.runExchange(aa, client, exchange, access, headers) catch |err| blk: { try out.print("note: token exchange failed ({t}); will retry on first use\n", .{err}); break :blk null; }; } } try panto.saveTokenSet(allocator, io, auth_dir, name, ts); try out.print("\nauthorized — '{s}' is ready to use.\n", .{name}); } // --------------------------------------------------------------------------- // `panto lua` argv plumbing — sketched against the older Args API for // reference (kept here so the design notes survive the implementation). // --------------------------------------------------------------------------- // // Because we own the `lua_State` end-to-end, the subcommand can also // expose extra panto-specific globals to user code (e.g. surface the // resolved $PANTO_HOME) without disturbing upstream `lua.c` behavior. // Step out of scope for the current makeover; add when needed. // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- const testing = std.testing; // Note: `dispatch` reads from the process's real argv, which isn't // controllable from a unit test. The behavior is exercised by // integration runs of the panto binary. We test the smaller pieces. // // Suppress dead-code warnings for `self_exe` (it's used by main, not // by tests in this module). test { _ = self_exe; }