diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 207d84699..ea52e7435 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -1165,7 +1165,8 @@ fn synthesizeSave(self: *Agent, arena: std.mem.Allocator, filename: ?[]const u8, fn saveOneShot(self: *Agent) void { var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); - self.synthesizeSaveTo(arena.allocator(), self.one_shot_save.?, .replace, self.one_shot_task.?); + const path = save.ensureJsExtension(arena.allocator(), self.one_shot_save.?) catch self.one_shot_save.?; + self.synthesizeSaveTo(arena.allocator(), path, .replace, self.one_shot_task.?); } /// LLM synthesis + write for an already-resolved destination. Shared by the diff --git a/src/agent/save.zig b/src/agent/save.zig index ab3d23822..09c2c8551 100644 --- a/src/agent/save.zig +++ b/src/agent/save.zig @@ -53,11 +53,19 @@ pub fn parseCommand(arena: std.mem.Allocator, rest: []const u8) !Command { after = trimmed[tok_end..]; } if (name.len == 0) return error.EmptyFilename; - if (!std.mem.endsWith(u8, name, ".js")) { - name = try std.mem.concat(arena, u8, &.{ name, ".js" }); - } const prompt = std.mem.trim(u8, after, &std.ascii.whitespace); - return .{ .filename = name, .prompt = if (prompt.len == 0) null else prompt }; + return .{ + .filename = try ensureJsExtension(arena, name), + .prompt = if (prompt.len == 0) null else prompt, + }; +} + +/// `name` with `.js` appended when missing; may alias `name` or be +/// arena-allocated. Shared by `/save` parsing and the one-shot `--save` flag +/// so the two paths can't drift. +pub fn ensureJsExtension(arena: std.mem.Allocator, name: []const u8) ![]const u8 { + if (std.mem.endsWith(u8, name, ".js")) return name; + return std.mem.concat(arena, u8, &.{ name, ".js" }); } pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 { @@ -119,6 +127,14 @@ test "parseCommand: filename only" { try std.testing.expect(r.prompt == null); } +test "ensureJsExtension appends only when missing" { + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + try std.testing.expectEqualStrings("out.js", try ensureJsExtension(arena.allocator(), "out")); + try std.testing.expectEqualStrings("out.js", try ensureJsExtension(arena.allocator(), "out.js")); + try std.testing.expectEqualStrings("a/b.thing.js", try ensureJsExtension(arena.allocator(), "a/b.thing")); +} + test "parseCommand: filename and prompt" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit();