fetch_ml/cli/src/commands/exec/remote.zig
Jeremie Fraeys 6316e4d702
refactor(cli): modularize exec.zig (533 lines)
Break down exec.zig into focused modules:
- exec/mod.zig - Main entry point and command dispatch (211 lines)
- exec/remote.zig - Remote execution via WebSocket (87 lines)
- exec/local.zig - Local execution with fork/exec (137 lines)
- exec/dryrun.zig - Dry-run preview functionality (53 lines)

Original exec.zig now acts as backward-compatible wrapper.

Benefits:
- Each module <150 lines (highly maintainable)
- Clear separation: remote vs local vs dry-run logic
- Easier to test individual execution paths
- Original 533-line file split into 4 focused modules

All tests pass.
2026-03-05 09:59:00 -05:00

116 lines
3.6 KiB
Zig

const std = @import("std");
const config = @import("../../config.zig");
const ws = @import("../../net/ws/client.zig");
const crypto = @import("../../utils/crypto.zig");
const protocol = @import("../../net/protocol.zig");
const history = @import("../../utils/history.zig");
/// Execute job on remote server
pub fn execute(
allocator: std.mem.Allocator,
job_name: []const u8,
priority: u8,
options: anytype,
args_str: []const u8,
cfg: config.Config,
) !void {
// Use queue command logic for remote execution
std.log.info("Queueing job on remote server: {s}", .{job_name});
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
// Generate commit ID
var commit_bytes: [20]u8 = undefined;
std.crypto.random.bytes(&commit_bytes);
// Build narrative JSON if provided
const narrative_json = buildNarrativeJson(allocator, options) catch null;
defer if (narrative_json) |j| allocator.free(j);
// Send queue request
try client.sendQueueJobWithArgsAndResources(
job_name,
&commit_bytes,
priority,
api_key_hash,
args_str,
false, // force
options.cpu,
options.memory,
options.gpu,
options.gpu_memory,
);
// Receive response
const message = try client.receiveMessage(allocator);
defer allocator.free(message);
const packet = protocol.ResponsePacket.deserialize(message, allocator) catch {
std.debug.print("Server response: {s}\n", .{message});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.success => {
const commit_hex = try crypto.encodeHexLower(allocator, &commit_bytes);
defer allocator.free(commit_hex);
history.record(allocator, job_name, commit_hex) catch {};
std.debug.print("Job queued: {s} (commit: {s})\n", .{ job_name, commit_hex[0..8] });
},
.error_packet => {
const err_msg = packet.error_message orelse "Unknown error";
std.debug.print("Error: {s}\n", .{err_msg});
return error.ServerError;
},
else => {
std.debug.print("Job queued: {s}\n", .{job_name});
},
}
}
fn buildNarrativeJson(allocator: std.mem.Allocator, options: anytype) !?[]const u8 {
if (options.hypothesis == null and options.context == null and
options.intent == null and options.expected_outcome == null)
{
return null;
}
var buf = try std.ArrayList(u8).initCapacity(allocator, 256);
defer buf.deinit(allocator);
const writer = buf.writer(allocator);
try writer.writeAll("{");
var first = true;
if (options.hypothesis) |h| {
if (!first) try writer.writeAll(",");
try writer.print("\"hypothesis\":\"{s}\"", .{h});
first = false;
}
if (options.context) |c| {
if (!first) try writer.writeAll(",");
try writer.print("\"context\":\"{s}\"", .{c});
first = false;
}
if (options.intent) |i| {
if (!first) try writer.writeAll(",");
try writer.print("\"intent\":\"{s}\"", .{i});
first = false;
}
if (options.expected_outcome) |eo| {
if (!first) try writer.writeAll(",");
try writer.print("\"expected_outcome\":\"{s}\"", .{eo});
first = false;
}
try writer.writeAll("}");
return try buf.toOwnedSlice(allocator);
}