diff --git a/cli/src/commands/common.zig b/cli/src/commands/common.zig index 59e1eb6..76fa275 100644 --- a/cli/src/commands/common.zig +++ b/cli/src/commands/common.zig @@ -60,3 +60,101 @@ pub fn withConnection( pub fn handleConnectionError(err: anyerror, operation_name: []const u8) void { std.debug.print("Failed to {s}: {}\n", .{ operation_name, err }); } + +/// Options for job operations +pub const JobOptions = struct { + cpu: u8 = 1, + memory: u8 = 4, + gpu: u8 = 0, + gpu_memory: ?[]const u8 = null, + dry_run: bool = false, + validate: bool = false, + explain: bool = false, + json: bool = false, + force: bool = false, + hypothesis: ?[]const u8 = null, + context: ?[]const u8 = null, + intent: ?[]const u8 = null, + expected_outcome: ?[]const u8 = null, + tags: ?[]const u8 = null, +}; + +/// Build narrative JSON from options +pub 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); +} + +/// Show dry run preview for job +pub fn dryRun( + _allocator: std.mem.Allocator, + job_name: []const u8, + exec_mode: anytype, + options: anytype, + args_str: []const u8, +) !void { + _ = _allocator; + std.debug.print("Dry run for job: {s}\n", .{job_name}); + std.debug.print(" Mode: {s}\n", .{@tagName(exec_mode)}); + std.debug.print(" CPU: {d}, Memory: {d}GB, GPU: {d}\n", .{ options.cpu, options.memory, options.gpu }); + if (args_str.len > 0) { + std.debug.print(" Args: {s}\n", .{args_str}); + } + if (options.hypothesis) |h| { + std.debug.print(" Hypothesis: {s}\n", .{h}); + } + std.debug.print("\n Action: Would {s}\n", .{ + switch (exec_mode) { + .local => "execute locally and mark for sync", + .remote => "queue on remote server", + }, + }); +} + +/// Format next steps message +pub fn formatNextSteps(allocator: std.mem.Allocator, job_name: []const u8, commit_hex: []const u8) ![]const u8 { + var out = try std.ArrayList(u8).initCapacity(allocator, 128); + errdefer out.deinit(allocator); + + const writer = out.writer(allocator); + try writer.writeAll("Next steps:\n"); + try writer.writeAll("\tml status --watch\n"); + try writer.print("\tml cancel {s}\n", .{job_name}); + try writer.print("\tml validate {s}\n", .{commit_hex}); + + return out.toOwnedSlice(allocator); +} diff --git a/cli/src/commands/queue.zig b/cli/src/commands/queue.zig index ca0b591..256a0d8 100644 --- a/cli/src/commands/queue.zig +++ b/cli/src/commands/queue.zig @@ -9,6 +9,7 @@ const mode = @import("../mode.zig"); const db = @import("../db.zig"); const manifest_lib = @import("../manifest.zig"); const progress = @import("../utils/progress.zig"); +const common = @import("common.zig"); // Use modular queue structure const queue_mod = @import("queue/mod.zig"); @@ -460,7 +461,7 @@ fn queueSingleJob( defer if (commit_override == null) allocator.free(commit_id); // Build narrative JSON if any narrative fields are set - const narrative_json = buildNarrativeJson(allocator, options) catch null; + const narrative_json = common.buildNarrativeJson(allocator, options) catch null; defer if (narrative_json) |j| allocator.free(j); const config = try Config.load(allocator); @@ -627,7 +628,7 @@ fn queueSingleJob( } else { std.debug.print("Job queued: {s}\n", .{job_name}); if (print_next_steps) { - const next_steps = try formatNextSteps(allocator, job_name, commit_hex); + const next_steps = try common.formatNextSteps(allocator, job_name, commit_hex); defer allocator.free(next_steps); std.debug.print("{s}\n", .{next_steps}); } @@ -648,7 +649,7 @@ fn queueSingleJob( std.debug.print("Warning: failed to record job in history ({})\n", .{err}); }; if (print_next_steps) { - const next_steps = try formatNextSteps(allocator, job_name, commit_hex); + const next_steps = try common.formatNextSteps(allocator, job_name, commit_hex); defer allocator.free(next_steps); std.debug.print("{s}\n", .{next_steps}); } @@ -707,19 +708,6 @@ fn printUsage() !void { std.debug.print("\t\t--context 'Following paper XYZ' --tags ablation,lr-scaling\n", .{}); } -pub fn formatNextSteps(allocator: std.mem.Allocator, job_name: []const u8, commit_hex: []const u8) ![]u8 { - var out = try std.ArrayList(u8).initCapacity(allocator, 128); - errdefer out.deinit(allocator); - - const writer = out.writer(allocator); - try writer.writeAll("Next steps:\n"); - try writer.writeAll("\tml status --watch\n"); - try writer.print("\tml cancel {s}\n", .{job_name}); - try writer.print("\tml validate {s}\n", .{commit_hex}); - - return out.toOwnedSlice(allocator); -} - fn explainJob( allocator: std.mem.Allocator, job_name: []const u8, @@ -737,7 +725,7 @@ fn explainJob( } // Build narrative JSON for display - const narrative_json = buildNarrativeJson(allocator, options) catch null; + const narrative_json = common.buildNarrativeJson(allocator, options) catch null; defer if (narrative_json) |j| allocator.free(j); if (options.json) { @@ -865,7 +853,7 @@ fn dryRunJob( } // Build narrative JSON for display - const narrative_json = buildNarrativeJson(allocator, options) catch null; + const narrative_json = common.buildNarrativeJson(allocator, options) catch null; defer if (narrative_json) |j| allocator.free(j); if (options.json) { @@ -1135,74 +1123,6 @@ fn hexDigit(v: u8) u8 { return if (v < 10) ('0' + v) else ('a' + (v - 10)); } -// buildNarrativeJson creates a JSON object from narrative fields -fn buildNarrativeJson(allocator: std.mem.Allocator, options: *const QueueOptions) !?[]u8 { - // Check if any narrative field is set - if (options.hypothesis == null and - options.context == null and - options.intent == null and - options.expected_outcome == null and - options.experiment_group == null and - options.tags == 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(","); - first = false; - try writer.writeAll("\"hypothesis\":"); - try writeJSONString(writer, h); - } - - if (options.context) |c| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"context\":"); - try writeJSONString(writer, c); - } - - if (options.intent) |i| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"intent\":"); - try writeJSONString(writer, i); - } - - if (options.expected_outcome) |eo| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"expected_outcome\":"); - try writeJSONString(writer, eo); - } - - if (options.experiment_group) |eg| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"experiment_group\":"); - try writeJSONString(writer, eg); - } - - if (options.tags) |t| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"tags\":"); - try writeJSONString(writer, t); - } - - try writer.writeAll("}"); - - return try buf.toOwnedSlice(allocator); -} - /// Check if a job with the same commit_id already exists on the server /// Returns: Optional JSON response from server if duplicate found fn checkExistingJob(