agent: allow paths in --save option

This commit is contained in:
Adrià Arrufat
2026-06-19 07:46:42 +02:00
parent 69d302fabe
commit 98a5458205
3 changed files with 10 additions and 18 deletions

View File

@@ -294,12 +294,12 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
});
return error.ConflictingFlags;
}
save.validateFilename(save_path) catch {
if (save_path.len == 0) {
log.fatal(.app, "invalid --save filename", .{
.hint = "--save takes a local file name, not a path (e.g. script.js)",
.hint = "--save needs a non-empty file name",
});
return error.InvalidFilename;
};
}
}
if (opts.no_llm and opts.provider != null) {
log.warn(.app, "ignoring --provider", .{ .reason = "--no-llm takes precedence" });
@@ -1013,7 +1013,6 @@ fn handleSave(self: *Agent, arena: std.mem.Allocator, rest: []const u8) void {
const msg: []const u8 = switch (err) {
error.UnterminatedQuote => "unterminated filename quote",
error.EmptyFilename => "filename cannot be empty",
error.InvalidFilename => "filename must be a local file name, not a path",
error.OutOfMemory => "out of memory",
};
self.terminal.printError("{s}", .{msg});

View File

@@ -52,7 +52,7 @@ pub fn parseCommand(arena: std.mem.Allocator, rest: []const u8) !Command {
name = trimmed[0..tok_end];
after = trimmed[tok_end..];
}
try validateFilename(name);
if (name.len == 0) return error.EmptyFilename;
if (!std.mem.endsWith(u8, name, ".js")) {
name = try std.mem.concat(arena, u8, &.{ name, ".js" });
}
@@ -60,14 +60,6 @@ pub fn parseCommand(arena: std.mem.Allocator, rest: []const u8) !Command {
return .{ .filename = name, .prompt = if (prompt.len == 0) null else prompt };
}
pub fn validateFilename(name: []const u8) !void {
if (name.len == 0) return error.EmptyFilename;
if (std.fs.path.isAbsolute(name)) return error.InvalidFilename;
if (std.mem.indexOfScalar(u8, name, '/') != null) return error.InvalidFilename;
if (std.mem.indexOfScalar(u8, name, '\\') != null) return error.InvalidFilename;
if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.InvalidFilename;
}
pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 {
for (0..100) |_| {
const n = std.crypto.random.int(u64);
@@ -175,12 +167,13 @@ test "parseCommand: empty is all null" {
try std.testing.expect(r.prompt == null);
}
test "parseCommand: rejects path-like filenames" {
test "parseCommand: accepts path-like filenames" {
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
const aa = arena.allocator();
try std.testing.expectError(error.InvalidFilename, parseCommand(aa, "../evil.js"));
try std.testing.expectError(error.InvalidFilename, parseCommand(aa, "/tmp/x.js"));
try std.testing.expectEqualStrings("../evil.js", (try parseCommand(aa, "../evil.js")).filename.?);
try std.testing.expectEqualStrings("/tmp/x.js", (try parseCommand(aa, "/tmp/x.js")).filename.?);
try std.testing.expectEqualStrings("out/run.js", (try parseCommand(aa, "out/run")).filename.?);
try std.testing.expectError(error.UnterminatedQuote, parseCommand(aa, "\"unclosed.js"));
}

View File

@@ -208,8 +208,8 @@
\\--save <PATH> 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. PATH must be a
\\ local file name, not a path. Requires --task.
\\ calls). Overwrites PATH if it exists. Requires
\\ --task.
\\
\\-a, --attach <PATH> Feed a local file to the model alongside --task.
\\ Repeatable, one file per flag. Text files are