//! 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 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, "--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 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 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; }