summaryrefslogtreecommitdiff
path: root/src/subcommand.zig
blob: f0fdfc9040ab56b11a8b2d3e4d7c189d0c4e265b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//! 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 <id>    Resume the conversation whose id begins with <id>.
        \\  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.<name>], pick a default with
        \\  [defaults] model = "<provider>:<alias>", 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):
///   <short-id>  <created>  <message-count> messages
///
/// where `<short-id>` 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, cwd);
    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;
}