const std = @import("std"); const process_supervisor = @import("../background/process_supervisor.zig"); const types = @import("../shared/types.zig"); const command_output_content = @import("../tooling/command_output_content.zig"); pub const CommandOutputStream = command_output_content.Stream; pub const CommandOutputCallback = command_output_content.Callback; pub const BackgroundCommand = struct { pid: []const u8, process_token: ?process_supervisor.ProcessInstanceToken = null, background_record_id: ?types.StableBackgroundRecordId = null, command: []const u8, cwd: []const u8, log_path: []const u8, url: ?[]const u8 = null, expect_url: bool = false, }; pub const ForegroundCommandResult = struct { command: []const u8, cwd: []const u8, exit_code: ?i64 = null, signal: ?u32 = null, timed_out: bool = false, duration_ms: ?u64 = null, stdout_bytes: usize = 0, stderr_bytes: usize = 0, truncated: bool = false, output_file: ?[]const u8 = null, stdout_file: ?[]const u8 = null, stderr_file: ?[]const u8 = null, }; pub const BackgroundCommandResult = struct { command: []const u8, cwd: []const u8, background_id: ?u64 = null, pid: []const u8, log_path: []const u8, state: []const u8 = "running", server_url: ?[]const u8 = null, }; pub const CommandResult = union(enum) { foreground: ForegroundCommandResult, background: BackgroundCommandResult, pub fn writeJson(self: CommandResult, writer: *std.Io.Writer) !void { switch (self) { .foreground => |result| try writeForegroundJson(result, writer), .background => |result| try writeBackgroundJson(result, writer), } } pub fn toJson(self: CommandResult, alloc: std.mem.Allocator) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try self.writeJson(&out.writer); return try out.toOwnedSlice(); } }; pub const RunCommandResult = struct { output: []const u8, background: ?BackgroundCommand = null, command_result: ?CommandResult = null, cancelled: bool = false, }; pub const ForegroundCommandStatus = union(enum) { exit_code: i64, signal: u32, finished, }; pub const ForegroundCommandResultSnapshot = struct { command: []const u8, cwd: []const u8, status: ForegroundCommandStatus, stdout_display: []const u8, stderr_display: []const u8, stdout_bytes: usize, stderr_bytes: usize, duration_ms: ?u64 = null, }; pub fn formatForegroundCommandResult( alloc: std.mem.Allocator, snapshot: ForegroundCommandResultSnapshot, ) !RunCommandResult { const stdout_text = std.mem.trim(u8, snapshot.stdout_display, " \r\n\t"); const stderr_text = std.mem.trim(u8, snapshot.stderr_display, " \r\n\t"); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeForegroundStatusLine(&out.writer, snapshot.status); try writeForegroundOutputEnvelopes(&out.writer, stdout_text, stderr_text); return .{ .output = try out.toOwnedSlice(), .command_result = .{ .foreground = .{ .command = snapshot.command, .cwd = snapshot.cwd, .exit_code = foregroundExitCode(snapshot.status), .signal = foregroundSignal(snapshot.status), .duration_ms = snapshot.duration_ms, .stdout_bytes = snapshot.stdout_bytes, .stderr_bytes = snapshot.stderr_bytes, } }, }; } fn writeForegroundStatusLine(writer: *std.Io.Writer, status: ForegroundCommandStatus) !void { switch (status) { .exit_code => |code| try writer.print("exit_code={d}\n", .{code}), .signal => |signal| try writer.print("signal={d}\n", .{signal}), .finished => try writer.writeAll("process finished\n"), } } fn writeForegroundOutputEnvelopes(writer: *std.Io.Writer, stdout_text: []const u8, stderr_text: []const u8) !void { if (stdout_text.len > 0) { try writer.writeAll("\n"); try writer.writeAll(stdout_text); try writer.writeAll("\n\n"); } if (stderr_text.len > 0) { try writer.writeAll("\n"); try writer.writeAll(stderr_text); try writer.writeAll("\n\n"); } if (stdout_text.len == 0 and stderr_text.len == 0) { try writer.writeAll("(no output)\n"); } } fn foregroundExitCode(status: ForegroundCommandStatus) ?i64 { return switch (status) { .exit_code => |code| code, else => null, }; } fn foregroundSignal(status: ForegroundCommandStatus) ?u32 { return switch (status) { .signal => |signal| signal, else => null, }; } fn writeForegroundJson(result: ForegroundCommandResult, writer: *std.Io.Writer) !void { try writer.writeAll("{\"kind\":\"foreground\""); try writeStringField(writer, "command", result.command); try writeStringField(writer, "cwd", result.cwd); try writeOptionalIntField(writer, "exit_code", result.exit_code); try writeOptionalIntField(writer, "signal", result.signal); try writeBoolField(writer, "timed_out", result.timed_out); try writeOptionalIntField(writer, "duration_ms", result.duration_ms); try writeIntField(writer, "stdout_bytes", result.stdout_bytes); try writeIntField(writer, "stderr_bytes", result.stderr_bytes); try writeBoolField(writer, "truncated", result.truncated); try writeOptionalStringField(writer, "output_file", result.output_file); try writeOptionalStringField(writer, "stdout_file", result.stdout_file); try writeOptionalStringField(writer, "stderr_file", result.stderr_file); try writer.writeByte('}'); } fn writeBackgroundJson(result: BackgroundCommandResult, writer: *std.Io.Writer) !void { try writer.writeAll("{\"kind\":\"background\""); try writeStringField(writer, "command", result.command); try writeStringField(writer, "cwd", result.cwd); try writeOptionalIntField(writer, "background_id", result.background_id); try writeStringField(writer, "pid", result.pid); try writeStringField(writer, "log_path", result.log_path); try writeStringField(writer, "state", result.state); try writeOptionalStringField(writer, "server_url", result.server_url); try writer.writeByte('}'); } fn writeStringField(writer: *std.Io.Writer, comptime name: []const u8, value: []const u8) !void { try writer.writeAll(",\"" ++ name ++ "\":"); try std.json.Stringify.value(value, .{}, writer); } fn writeOptionalStringField(writer: *std.Io.Writer, comptime name: []const u8, value: ?[]const u8) !void { try writer.writeAll(",\"" ++ name ++ "\":"); if (value) |text| { try std.json.Stringify.value(text, .{}, writer); } else { try writer.writeAll("null"); } } fn writeBoolField(writer: *std.Io.Writer, comptime name: []const u8, value: bool) !void { try writer.writeAll(",\"" ++ name ++ "\":"); try writer.writeAll(if (value) "true" else "false"); } fn writeIntField(writer: *std.Io.Writer, comptime name: []const u8, value: anytype) !void { try writer.writeAll(",\"" ++ name ++ "\":"); try writer.print("{d}", .{value}); } fn writeOptionalIntField(writer: *std.Io.Writer, comptime name: []const u8, value: anytype) !void { try writer.writeAll(",\"" ++ name ++ "\":"); if (value) |number| { try writer.print("{d}", .{number}); } else { try writer.writeAll("null"); } } test "foreground result preserves envelopes metadata and json" { const result = try formatForegroundCommandResult(std.testing.allocator, .{ .command = "printf hello", .cwd = "/tmp", .status = .{ .exit_code = 7 }, .stdout_display = " hello\n", .stderr_display = " warn\n", .stdout_bytes = 7, .stderr_bytes = 6, .duration_ms = 12, }); defer std.testing.allocator.free(result.output); try std.testing.expectEqualStrings( "exit_code=7\n\nhello\n\n\nwarn\n\n", result.output, ); const foreground = result.command_result.?.foreground; try std.testing.expectEqualStrings("printf hello", foreground.command); try std.testing.expectEqualStrings("/tmp", foreground.cwd); try std.testing.expectEqual(@as(?i64, 7), foreground.exit_code); try std.testing.expectEqual(@as(?u32, null), foreground.signal); try std.testing.expectEqual(@as(?u64, 12), foreground.duration_ms); try std.testing.expectEqual(@as(usize, 7), foreground.stdout_bytes); try std.testing.expectEqual(@as(usize, 6), foreground.stderr_bytes); const json = try result.command_result.?.toJson(std.testing.allocator); defer std.testing.allocator.free(json); try std.testing.expectEqualStrings( "{\"kind\":\"foreground\",\"command\":\"printf hello\",\"cwd\":\"/tmp\",\"exit_code\":7,\"signal\":null,\"timed_out\":false,\"duration_ms\":12,\"stdout_bytes\":7,\"stderr_bytes\":6,\"truncated\":false,\"output_file\":null,\"stdout_file\":null,\"stderr_file\":null}", json, ); } test "foreground result preserves empty finished output" { const result = try formatForegroundCommandResult(std.testing.allocator, .{ .command = "cmd", .cwd = "/tmp", .status = .finished, .stdout_display = "", .stderr_display = "", .stdout_bytes = 0, .stderr_bytes = 0, }); defer std.testing.allocator.free(result.output); try std.testing.expectEqualStrings("process finished\n(no output)\n", result.output); }