diff --git a/src/App.zig b/src/App.zig index 4f339803f..947e2e2e2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -78,7 +78,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App { app.app_dir_path = getAndMakeAppDir(allocator); - app.telemetry = try Telemetry.init(app, config.mode, config.interactive()); + app.telemetry = try Telemetry.init(app, config.command, config.interactive()); errdefer app.telemetry.deinit(allocator); app.arena_pool = ArenaPool.init(allocator, .{}); diff --git a/src/Config.zig b/src/Config.zig index cc825192b..726f95b03 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -263,6 +263,13 @@ const Commands = cli.Builder(.{ }, .shared_options = CommonOptions, }, + .{ + // Normalized to `.agent` in `parseArgs`; intentionally no LLM options. + .name = "run", + .positional = .{ .name = "script_file", .type = ?[:0]const u8 }, + .options = .{}, + .shared_options = CommonOptions, + }, .{ .name = "version", .options = .{ .{ .name = "check", .type = bool }, } }, @@ -273,6 +280,9 @@ pub const Mode = Commands.Union; pub const Agent = @FieldType(Mode, "agent"); mode: Mode, +// The command as typed. Mirrors `mode`, except `run` normalizes to `.agent` +// for execution while this keeps `.run` for telemetry. +command: RunMode, exec_name: []const u8, http_headers: HttpHeaders, @@ -287,6 +297,7 @@ fn modeNeedsHttp(mode: Mode) bool { pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config { var config = Config{ .mode = mode, + .command = std.meta.activeTag(mode), .exec_name = exec_name, .http_headers = undefined, }; @@ -713,7 +724,7 @@ pub fn printUsageAndExit(self: *const Config, help_for: RunMode, success: bool) , .{Help.general}); std.debug.print(template, .{exec_name}); }, - inline .fetch, .serve, .mcp, .agent => |tag| { + inline .fetch, .serve, .mcp, .agent, .run => |tag| { const template = comptimePrint( \\{s} \\ @@ -735,11 +746,28 @@ pub fn printUsageAndExit(self: *const Config, help_for: RunMode, success: bool) } pub fn parseArgs(allocator: Allocator) !Config { - const exec_name, const command = try Commands.parse(allocator); + const exec_name, var command = try Commands.parse(allocator); if (command == .serve and command.serve.timeout != null) { log.warn(.app, "--timeout is deprecated", .{}); } - return .init(allocator, exec_name, command); + const invoked = std.meta.activeTag(command); + // Rewrite `run` to `.agent` so nothing downstream needs a `.run` case. + if (command == .run) { + const run = command.run; + if (run.script_file == null) { + log.fatal(.app, "missing script file", .{ .hint = "usage: lightpanda run " }); + return error.MissingArgument; + } + // run's fields are a strict subset of Agent's (compile error otherwise). + var agent_opts: Agent = .{}; + inline for (@typeInfo(@TypeOf(run)).@"struct".fields) |f| { + @field(agent_opts, f.name) = @field(run, f.name); + } + command = .{ .agent = agent_opts }; + } + var config = try Config.init(allocator, exec_name, command); + config.command = invoked; + return config; } pub fn validateUserAgent(ua: []const u8) !void { diff --git a/src/help.zon b/src/help.zon index 1163f001c..eb4023a9c 100644 --- a/src/help.zon +++ b/src/help.zon @@ -8,6 +8,7 @@ \\ fetch fetches the specified URL \\ help displays this message \\ mcp starts an MCP (Model Context Protocol) server over stdio + \\ run runs a saved script (no LLM), then exits \\ serve starts a WebSocket CDP server \\ version displays the version of {0s} \\ @@ -143,14 +144,15 @@ \\ {0s} agent --provider huggingface (HF serverless router, HF_TOKEN) \\ {0s} agent --provider ollama --model qwen3.5:latest \\ {0s} agent --no-llm (basic slash-command-only REPL) - \\ {0s} agent script.js (run a saved script, then exit) + \\ {0s} run script.js (replay a saved script; see `run`) \\ {0s} agent --task "..." --save out.js (synthesize a replayable script) \\ \\Arguments: \\[SCRIPT] \\ Optional path to a .js script. Runs the script (no LLM calls) and - \\ exits. With no script and no --task, the REPL starts; from there - \\ /load runs a script and /save exports the session to a file. + \\ exits; `{0s} run SCRIPT` is the preferred spelling. With no script + \\ and no --task, the REPL starts; from there /load runs a script and + \\ /save exports the session to a file. \\ Caution: .js files can contain evaluate(...) calls that run \\ arbitrary JavaScript in the page. Only run scripts you trust, the \\ same way you would a shell script. @@ -209,7 +211,7 @@ \\ --save \\ Synthesize a replayable .js script from the --task run and write \\ it to PATH, instead of printing the answer. Replay it later with - \\ `agent PATH` (no LLM calls). Overwrites PATH if it exists. + \\ `run PATH` (no LLM calls). Overwrites PATH if it exists. \\ Requires --task. \\ --system-prompt \\ Override the default system prompt. @@ -237,6 +239,25 @@ \\MISTRAL_API_KEY. The local servers (Ollama, llama.cpp) do not require an \\API key. , + .run = + \\run command + \\Runs a saved script, then exits. No LLM calls, no API key needed. + \\ + \\Usage: + \\ {0s} run