style(cli): Standardize printUsage() formatting with tabs and ASCII symbols

Replace space-padding with consistent tab (\t) alignment in all printUsage() functions.

Add ligature-friendly ASCII symbols:

  - => for results/outcomes (renders as ⇒ with ligatures)

  - ~> for modifications/changes (renders as ~> with ligatures)

  - -> for state transitions (renders as → with ligatures)

  - [OK] / [FAIL] for status indicators

All symbols use ASCII 32-126 for xargs-safe, copy-pasteable output.
This commit is contained in:
Jeremie Fraeys 2026-02-23 14:09:49 -05:00
parent 3b194ff2e8
commit a1988de8b1
No known key found for this signature in database
17 changed files with 709 additions and 874 deletions

View file

@ -2,7 +2,6 @@ const std = @import("std");
const config = @import("../config.zig");
const db = @import("../db.zig");
const core = @import("../core.zig");
const colors = @import("../utils/colors.zig");
const manifest_lib = @import("../manifest.zig");
/// Annotate command - unified metadata annotation
@ -16,7 +15,7 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
var command_args = try core.flags.parseCommon(allocator, args, &flags);
defer command_args.deinit(allocator);
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
if (flags.help) {
return printUsage();
@ -96,7 +95,7 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (flags.json) {
std.debug.print("{{\"success\":true,\"run_id\":\"{s}\",\"action\":\"note_added\"}}\n", .{run_id});
} else {
colors.printSuccess("Added note to run {s}\n", .{run_id[0..8]});
std.debug.print("Added note to run {s}\n", .{run_id[0..8]});
}
}
@ -128,16 +127,16 @@ fn printUsage() !void {
std.debug.print("Usage: ml annotate <run_id> [options]\n\n", .{});
std.debug.print("Add metadata annotations to a run.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --text <string> Free-form annotation\n", .{});
std.debug.print(" --hypothesis <string> Research hypothesis\n", .{});
std.debug.print(" --outcome <status> Outcome: validates/refutes/inconclusive\n", .{});
std.debug.print(" --confidence <0-1> Confidence in outcome\n", .{});
std.debug.print(" --privacy <level> Privacy: private/team/public\n", .{});
std.debug.print(" --author <name> Author of the annotation\n", .{});
std.debug.print(" --help, -h Show this help\n", .{});
std.debug.print(" --json Output structured JSON\n\n", .{});
std.debug.print("\t--text <string>\t\tFree-form annotation\n", .{});
std.debug.print("\t--hypothesis <string>\tResearch hypothesis\n", .{});
std.debug.print("\t--outcome <status>\tOutcome: validates/refutes/inconclusive\n", .{});
std.debug.print("\t--confidence <0-1>\tConfidence in outcome\n", .{});
std.debug.print("\t--privacy <level>\tPrivacy: private/team/public\n", .{});
std.debug.print("\t--author <name>\t\tAuthor of the annotation\n", .{});
std.debug.print("\t--help, -h\t\tShow this help\n", .{});
std.debug.print("\t--json\t\t\tOutput structured JSON\n\n", .{});
std.debug.print("Examples:\n", .{});
std.debug.print(" ml annotate abc123 --text \"Try lr=3e-4 next\"\n", .{});
std.debug.print(" ml annotate abc123 --hypothesis \"LR scaling helps\"\n", .{});
std.debug.print(" ml annotate abc123 --outcome validates --confidence 0.9\n", .{});
std.debug.print("\tml annotate abc123 --text \"Try lr=3e-4 next\"\n", .{});
std.debug.print("\tml annotate abc123 --hypothesis \"LR scaling helps\"\n", .{});
std.debug.print("\tml annotate abc123 --outcome validates --confidence 0.9\n", .{});
}

View file

@ -3,7 +3,6 @@ const config = @import("../config.zig");
const db = @import("../db.zig");
const ws = @import("../net/ws/client.zig");
const crypto = @import("../utils/crypto.zig");
const colors = @import("../utils/colors.zig");
const core = @import("../core.zig");
const mode = @import("../mode.zig");
const manifest_lib = @import("../manifest.zig");
@ -25,17 +24,17 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
return printUsage();
} else if (std.mem.startsWith(u8, arg, "--")) {
core.output.errorMsg("cancel", "Unknown option");
core.output.err("Unknown option");
return error.InvalidArgs;
} else {
try targets.append(allocator, arg);
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
if (targets.items.len == 0) {
core.output.errorMsg("cancel", "No run_id specified");
core.output.err("No run_id specified");
return error.InvalidArgs;
}
@ -59,7 +58,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Local mode: kill by PID
cancelLocal(allocator, target, force, flags.json) catch |err| {
if (!flags.json) {
colors.printError("Failed to cancel '{s}': {}\n", .{ target, err });
std.debug.print("Failed to cancel '{s}': {}\n", .{ target, err });
}
failed_count += 1;
continue;
@ -68,7 +67,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Online mode: cancel on server
cancelServer(allocator, target, force, flags.json, cfg) catch |err| {
if (!flags.json) {
colors.printError("Failed to cancel '{s}': {}\n", .{ target, err });
std.debug.print("Failed to cancel '{s}': {}\n", .{ target, err });
}
failed_count += 1;
continue;
@ -80,9 +79,9 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (flags.json) {
std.debug.print("{{\"success\":true,\"canceled\":{d},\"failed\":{d}}}\n", .{ success_count, failed_count });
} else {
colors.printSuccess("Canceled {d} run(s)\n", .{success_count});
std.debug.print("Canceled {d} run(s)\n", .{success_count});
if (failed_count > 0) {
colors.printError("Failed to cancel {d} run(s)\n", .{failed_count});
std.debug.print("Failed to cancel {d} run(s)\n", .{failed_count});
}
}
}
@ -163,7 +162,7 @@ fn cancelLocal(allocator: std.mem.Allocator, run_id: []const u8, force: bool, js
database.checkpointOnExit();
if (!json) {
colors.printSuccess("Canceled run {s}\n", .{run_id[0..8]});
std.debug.print("Canceled run {s}\n", .{run_id[0..8]});
}
}
@ -200,13 +199,13 @@ fn cancelServer(allocator: std.mem.Allocator, job_name: []const u8, force: bool,
}
fn printUsage() !void {
colors.printInfo("Usage: ml cancel [options] <run-id> [<run-id> ...]\n", .{});
colors.printInfo("\nCancel a local run (kill process) or server job.\n\n", .{});
colors.printInfo("Options:\n", .{});
colors.printInfo(" --force Force cancel (SIGKILL immediately)\n", .{});
colors.printInfo(" --json Output structured JSON\n", .{});
colors.printInfo(" --help, -h Show this help message\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml cancel abc123 # Cancel local run by run_id\n", .{});
colors.printInfo(" ml cancel --force abc123 # Force cancel\n", .{});
std.debug.print("Usage: ml cancel [options] <run-id> [<run-id> ...]\n", .{});
std.debug.print("\nCancel a local run (kill process) or server job.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print("\t--force\t\tForce cancel (SIGKILL immediately)\n", .{});
std.debug.print("\t--json\t\tOutput structured JSON\n", .{});
std.debug.print("\t--help, -h\tShow this help message\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print("\tml cancel abc123\t# Cancel local run by run_id\n", .{});
std.debug.print("\tml cancel --force abc123\t# Force cancel\n", .{});
}

View file

@ -1,5 +1,4 @@
const std = @import("std");
const colors = @import("../utils/colors.zig");
const Config = @import("../config.zig").Config;
const crypto = @import("../utils/crypto.zig");
const io = @import("../utils/io.zig");
@ -47,12 +46,12 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
return printUsage();
} else {
core.output.errorMsg("compare", "Unknown option");
core.output.err("Unknown option");
return error.InvalidArgs;
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
const cfg = try Config.load(allocator);
defer {
@ -67,7 +66,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
defer allocator.free(ws_url);
// Fetch both runs
colors.printInfo("Fetching run {s}...\n", .{run_a});
std.debug.print("Fetching run {s}...\n", .{run_a});
var client_a = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client_a.close();
@ -76,7 +75,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
const msg_a = try client_a.receiveMessage(allocator);
defer allocator.free(msg_a);
colors.printInfo("Fetching run {s}...\n", .{run_b});
std.debug.print("Fetching run {s}...\n", .{run_b});
var client_b = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client_b.close();
@ -86,13 +85,13 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
// Parse responses
const parsed_a = std.json.parseFromSlice(std.json.Value, allocator, msg_a, .{}) catch {
colors.printError("Failed to parse response for {s}\n", .{run_a});
std.debug.print("Failed to parse response for {s}\n", .{run_a});
return error.InvalidResponse;
};
defer parsed_a.deinit();
const parsed_b = std.json.parseFromSlice(std.json.Value, allocator, msg_b, .{}) catch {
colors.printError("Failed to parse response for {s}\n", .{run_b});
std.debug.print("Failed to parse response for {s}\n", .{run_b});
return error.InvalidResponse;
};
defer parsed_b.deinit();
@ -102,11 +101,11 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
// Check for errors
if (root_a.get("error")) |err_a| {
colors.printError("Error fetching {s}: {s}\n", .{ run_a, err_a.string });
std.debug.print("Error fetching {s}: {s}\n", .{ run_a, err_a.string });
return error.ServerError;
}
if (root_b.get("error")) |err_b| {
colors.printError("Error fetching {s}: {s}\n", .{ run_b, err_b.string });
std.debug.print("Error fetching {s}: {s}\n", .{ run_b, err_b.string });
return error.ServerError;
}
@ -124,30 +123,30 @@ fn outputHumanComparison(
run_b: []const u8,
all_fields: bool,
) !void {
colors.printInfo("\n=== Comparison: {s} vs {s} ===\n\n", .{ run_a, run_b });
std.debug.print("\n=== Comparison: {s} vs {s} ===\n\n", .{ run_a, run_b });
// Common fields
const job_name_a = jsonGetString(root_a, "job_name") orelse "unknown";
const job_name_b = jsonGetString(root_b, "job_name") orelse "unknown";
if (!std.mem.eql(u8, job_name_a, job_name_b)) {
colors.printWarning("Job names differ:\n", .{});
colors.printInfo(" {s}: {s}\n", .{ run_a, job_name_a });
colors.printInfo(" {s}: {s}\n", .{ run_b, job_name_b });
std.debug.print("Job names differ:\n", .{});
std.debug.print("\t{s}: {s}\n", .{ run_a, job_name_a });
std.debug.print("\t{s}: {s}\n", .{ run_b, job_name_b });
} else {
colors.printInfo("Job Name: {s}\n", .{job_name_a});
std.debug.print("Job Name: {s}\n", .{job_name_a});
}
// Experiment group
const group_a = jsonGetString(root_a, "experiment_group") orelse "";
const group_b = jsonGetString(root_b, "experiment_group") orelse "";
if (group_a.len > 0 or group_b.len > 0) {
colors.printInfo("\nExperiment Group:\n", .{});
std.debug.print("\nExperiment Group:\n", .{});
if (std.mem.eql(u8, group_a, group_b)) {
colors.printInfo(" Both: {s}\n", .{group_a});
std.debug.print("\tBoth: {s}\n", .{group_a});
} else {
colors.printInfo(" {s}: {s}\n", .{ run_a, group_a });
colors.printInfo(" {s}: {s}\n", .{ run_b, group_b });
std.debug.print("\t{s}: {s}\n", .{ run_a, group_a });
std.debug.print("\t{s}: {s}\n", .{ run_b, group_b });
}
}
@ -156,7 +155,7 @@ fn outputHumanComparison(
const narrative_b = root_b.get("narrative");
if (narrative_a != null or narrative_b != null) {
colors.printInfo("\n--- Narrative ---\n", .{});
std.debug.print("\n--- Narrative ---\n", .{});
if (narrative_a) |na| {
if (narrative_b) |nb| {
@ -164,10 +163,10 @@ fn outputHumanComparison(
try compareNarrativeFields(na.object, nb.object, run_a, run_b);
}
} else {
colors.printInfo(" {s} has narrative, {s} does not\n", .{ run_a, run_b });
std.debug.print("\t{s} has narrative, {s} does not\n", .{ run_a, run_b });
}
} else if (narrative_b) |_| {
colors.printInfo(" {s} has narrative, {s} does not\n", .{ run_b, run_a });
std.debug.print("\t{s} has narrative, {s} does not\n", .{ run_b, run_a });
}
}
@ -178,7 +177,7 @@ fn outputHumanComparison(
if (meta_a) |ma| {
if (meta_b) |mb| {
if (ma == .object and mb == .object) {
colors.printInfo("\n--- Metadata Differences ---\n", .{});
std.debug.print("\n--- Metadata Differences ---\n", .{});
try compareMetadata(ma.object, mb.object, run_a, run_b, all_fields);
}
}
@ -191,7 +190,7 @@ fn outputHumanComparison(
if (metrics_a) |ma| {
if (metrics_b) |mb| {
if (ma == .object and mb == .object) {
colors.printInfo("\n--- Metrics ---\n", .{});
std.debug.print("\n--- Metrics ---\n", .{});
try compareMetrics(ma.object, mb.object, run_a, run_b);
}
}
@ -201,16 +200,16 @@ fn outputHumanComparison(
const outcome_a = jsonGetString(root_a, "outcome") orelse "";
const outcome_b = jsonGetString(root_b, "outcome") orelse "";
if (outcome_a.len > 0 or outcome_b.len > 0) {
colors.printInfo("\n--- Outcome ---\n", .{});
std.debug.print("\n--- Outcome ---\n", .{});
if (std.mem.eql(u8, outcome_a, outcome_b)) {
colors.printInfo(" Both: {s}\n", .{outcome_a});
std.debug.print("\tBoth: {s}\n", .{outcome_a});
} else {
colors.printInfo(" {s}: {s}\n", .{ run_a, outcome_a });
colors.printInfo(" {s}: {s}\n", .{ run_b, outcome_b });
std.debug.print("\t{s}: {s}\n", .{ run_a, outcome_a });
std.debug.print("\t{s}: {s}\n", .{ run_b, outcome_b });
}
}
colors.printInfo("\n", .{});
std.debug.print("\n", .{});
}
fn outputJsonComparison(
@ -294,14 +293,14 @@ fn compareNarrativeFields(
if (val_a != null and val_b != null) {
if (!std.mem.eql(u8, val_a.?, val_b.?)) {
colors.printInfo(" {s}:\n", .{field});
colors.printInfo(" {s}: {s}\n", .{ run_a, val_a.? });
colors.printInfo(" {s}: {s}\n", .{ run_b, val_b.? });
std.debug.print("\t{s}:\n", .{field});
std.debug.print("\t\t{s}: {s}\n", .{ run_a, val_a.? });
std.debug.print("\t\t{s}: {s}\n", .{ run_b, val_b.? });
}
} else if (val_a != null) {
colors.printInfo(" {s}: only in {s}\n", .{ field, run_a });
std.debug.print("\t{s}: only in {s}\n", .{ field, run_a });
} else if (val_b != null) {
colors.printInfo(" {s}: only in {s}\n", .{ field, run_b });
std.debug.print("\t{s}: only in {s}\n", .{ field, run_b });
}
}
}
@ -326,22 +325,22 @@ fn compareMetadata(
if (!std.mem.eql(u8, str_a, str_b)) {
has_differences = true;
colors.printInfo(" {s}: {s} → {s}\n", .{ key, str_a, str_b });
std.debug.print("\t{s}: {s} ~> {s}\n", .{ key, str_a, str_b });
} else if (show_all) {
colors.printInfo(" {s}: {s} (same)\n", .{ key, str_a });
std.debug.print("\t{s}: {s} (same)\n", .{ key, str_a });
}
} else if (show_all) {
colors.printInfo(" {s}: only in {s}\n", .{ key, run_a });
std.debug.print("\t{s}: only in {s}\n", .{ key, run_a });
}
} else if (mb.get(key)) |_| {
if (show_all) {
colors.printInfo(" {s}: only in {s}\n", .{ key, run_b });
std.debug.print("\t{s}: only in {s}\n", .{ key, run_b });
}
}
}
if (!has_differences and !show_all) {
colors.printInfo(" (no significant differences in common metadata)\n", .{});
std.debug.print("\t(no significant differences in common metadata)\n", .{});
}
}
@ -366,9 +365,9 @@ fn compareMetrics(
const diff = val_b - val_a;
const percent = if (val_a != 0) (diff / val_a) * 100 else 0;
const arrow = if (diff > 0) "" else if (diff < 0) "" else "=";
const arrow = if (diff > 0) "+" else if (diff < 0) "-" else "=";
colors.printInfo(" {s}: {d:.4} → {d:.4} ({s}{d:.4}, {d:.1}%)\n", .{
std.debug.print(" {s}: {d:.4} ~> {d:.4} ({s}{d:.4}, {d:.1}%)\n", .{
metric, val_a, val_b, arrow, @abs(diff), percent,
});
}
@ -498,19 +497,19 @@ fn jsonValueToFloat(v: std.json.Value) f64 {
}
fn printUsage() !void {
colors.printInfo("Usage: ml compare <run-a> <run-b> [options]\n", .{});
colors.printInfo("\nCompare two runs and show differences in:\n", .{});
colors.printInfo(" - Job metadata (batch_size, learning_rate, etc.)\n", .{});
colors.printInfo(" - Narrative fields (hypothesis, context, intent)\n", .{});
colors.printInfo(" - Metrics (accuracy, loss, training_time)\n", .{});
colors.printInfo(" - Outcome status\n", .{});
colors.printInfo("\nOptions:\n", .{});
colors.printInfo(" --json Output as JSON\n", .{});
colors.printInfo(" --all Show all fields (including unchanged)\n", .{});
colors.printInfo(" --fields <csv> Compare only specific fields\n", .{});
colors.printInfo(" --help, -h Show this help\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml compare run_abc run_def\n", .{});
colors.printInfo(" ml compare run_abc run_def --json\n", .{});
colors.printInfo(" ml compare run_abc run_def --all\n", .{});
std.debug.print("Usage: ml compare <run-a> <run-b> [options]\n", .{});
std.debug.print("\nCompare two runs and show differences in:\n", .{});
std.debug.print("\t- Job metadata (batch_size, learning_rate, etc.)\n", .{});
std.debug.print("\t- Narrative fields (hypothesis, context, intent)\n", .{});
std.debug.print("\t- Metrics (accuracy, loss, training_time)\n", .{});
std.debug.print("\t- Outcome status\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print("\t--json\t\tOutput as JSON\n", .{});
std.debug.print("\t--all\t\tShow all fields (including unchanged)\n", .{});
std.debug.print("\t--fields <csv>\tCompare only specific fields\n", .{});
std.debug.print("\t--help, -h\tShow this help\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print("\tml compare run_abc run_def\n", .{});
std.debug.print("\tml compare run_abc run_def --json\n", .{});
std.debug.print("\tml compare run_abc run_def --all\n", .{});
}

View file

@ -1,8 +1,6 @@
const std = @import("std");
const Config = @import("../config.zig").Config;
const ws = @import("../net/ws/client.zig");
const colors = @import("../utils/colors.zig");
const logging = @import("../utils/logging.zig");
const crypto = @import("../utils/crypto.zig");
const core = @import("../core.zig");
const native_hash = @import("../native/hash.zig");
@ -42,14 +40,14 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, arg, "--csv")) {
csv = true;
} else if (std.mem.startsWith(u8, arg, "--")) {
core.output.errorMsg("dataset", "Unknown option");
core.output.err("Unknown option");
return printUsage();
} else {
try positional.append(allocator, arg);
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
const action = positional.items[0];
@ -87,7 +85,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
},
else => {
core.output.errorMsg("dataset", "Too many arguments");
core.output.err("Too many arguments");
return error.InvalidArgs;
},
}
@ -96,18 +94,18 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
fn printUsage() void {
colors.printInfo("Usage: ml dataset <action> [options]\n", .{});
colors.printInfo("\nActions:\n", .{});
colors.printInfo(" list List registered datasets\n", .{});
colors.printInfo(" register <name> <url> Register a dataset with URL\n", .{});
colors.printInfo(" info <name> Show dataset information\n", .{});
colors.printInfo(" search <term> Search datasets by name/description\n", .{});
colors.printInfo(" verify <path|id> Verify dataset integrity (auto-hashes)\n", .{});
colors.printInfo("\nOptions:\n", .{});
colors.printInfo(" --dry-run Show what would be requested\n", .{});
colors.printInfo(" --validate Validate inputs only (no request)\n", .{});
colors.printInfo(" --json Output machine-readable JSON\n", .{});
colors.printInfo(" --help, -h Show this help message\n", .{});
std.debug.print("Usage: ml dataset <action> [options]\n\n", .{});
std.debug.print("Actions:\n", .{});
std.debug.print("\tlist\t\t\tList registered datasets\n", .{});
std.debug.print("\tregister <name> <url>\tRegister a dataset with URL\n", .{});
std.debug.print("\tinfo <name>\t\tShow dataset information\n", .{});
std.debug.print("\tsearch <term>\t\tSearch datasets by name/description\n", .{});
std.debug.print("\tverify <path|id>\tVerify dataset integrity (auto-hashes)\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print("\t--dry-run\t\tShow what would be requested\n", .{});
std.debug.print("\t--validate\t\tValidate inputs only (no request)\n", .{});
std.debug.print("\t--json\t\t\tOutput machine-readable JSON\n", .{});
std.debug.print("\t--help, -h\t\tShow this help message\n", .{});
}
fn listDatasets(allocator: std.mem.Allocator, options: *const DatasetOptions) !void {
@ -128,7 +126,7 @@ fn listDatasets(allocator: std.mem.Allocator, options: *const DatasetOptions) !v
const formatted = std.fmt.bufPrint(&buffer, "{{\"ok\":true,\"action\":\"list\",\"validated\":true}}\n", .{}) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Validation OK\n", .{});
std.debug.print("Validation OK\n", .{});
}
return;
}
@ -146,7 +144,7 @@ fn listDatasets(allocator: std.mem.Allocator, options: *const DatasetOptions) !v
const formatted = std.fmt.bufPrint(&buffer, "{{\"dry_run\":true,\"action\":\"list\"}}\n", .{}) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Dry run: would request dataset list\n", .{});
std.debug.print("Dry run: would request dataset list\n", .{});
}
return;
}
@ -165,15 +163,13 @@ fn listDatasets(allocator: std.mem.Allocator, options: *const DatasetOptions) !v
return;
}
colors.printInfo("Registered Datasets:\n", .{});
colors.printInfo("=====================\n\n", .{});
std.debug.print("Registered Datasets:\n", .{});
// Parse and display datasets (simplified for now)
if (std.mem.eql(u8, response, "[]")) {
colors.printWarning("No datasets registered.\n", .{});
colors.printInfo("Use 'ml dataset register <name> <url>' to add a dataset.\n", .{});
std.debug.print("No datasets registered.\n", .{});
} else {
colors.printSuccess("{s}\n", .{response});
std.debug.print("{s}\n", .{response});
}
}
@ -204,7 +200,7 @@ fn registerDataset(allocator: std.mem.Allocator, name: []const u8, url: []const
const formatted = std.fmt.bufPrint(&buffer, "{{\"ok\":true,\"action\":\"register\",\"validated\":true,\"name\":\"{s}\",\"url\":\"{s}\"}}\n", .{ name, url }) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Validation OK\n", .{});
std.debug.print("Validation OK\n", .{});
}
return;
}
@ -213,7 +209,7 @@ fn registerDataset(allocator: std.mem.Allocator, name: []const u8, url: []const
if (!std.mem.startsWith(u8, url, "http://") and !std.mem.startsWith(u8, url, "https://") and
!std.mem.startsWith(u8, url, "s3://") and !std.mem.startsWith(u8, url, "gs://"))
{
colors.printError("Invalid URL format. Supported: http://, https://, s3://, gs://\n", .{});
std.debug.print("Invalid URL format. Supported: http://, https://, s3://, gs://\n", .{});
return error.InvalidURL;
}
@ -226,7 +222,7 @@ fn registerDataset(allocator: std.mem.Allocator, name: []const u8, url: []const
const formatted = std.fmt.bufPrint(&buffer, "{{\"dry_run\":true,\"action\":\"register\",\"name\":\"{s}\",\"url\":\"{s}\"}}\n", .{ name, url }) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Dry run: would register dataset '{s}' -> {s}\n", .{ name, url });
std.debug.print("Dry run: would register dataset '{s}' -> {s}\n", .{ name, url });
}
return;
}
@ -252,10 +248,9 @@ fn registerDataset(allocator: std.mem.Allocator, name: []const u8, url: []const
}
if (std.mem.startsWith(u8, response, "ERROR")) {
colors.printError("Failed to register dataset: {s}\n", .{response});
std.debug.print("Failed to register dataset: {s}\n", .{response});
} else {
colors.printSuccess("Dataset '{s}' registered successfully!\n", .{name});
colors.printInfo("URL: {s}\n", .{url});
std.debug.print("Dataset '{s}' registered\n", .{name});
}
}
@ -277,7 +272,7 @@ fn showDatasetInfo(allocator: std.mem.Allocator, name: []const u8, options: *con
const formatted = std.fmt.bufPrint(&buffer, "{{\"ok\":true,\"action\":\"info\",\"validated\":true,\"name\":\"{s}\"}}\n", .{name}) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Validation OK\n", .{});
std.debug.print("Validation OK\n", .{});
}
return;
}
@ -291,7 +286,7 @@ fn showDatasetInfo(allocator: std.mem.Allocator, name: []const u8, options: *con
const formatted = std.fmt.bufPrint(&buffer, "{{\"dry_run\":true,\"action\":\"info\",\"name\":\"{s}\"}}\n", .{name}) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Dry run: would request dataset info for '{s}'\n", .{name});
std.debug.print("Dry run: would request dataset info for '{s}'\n", .{name});
}
return;
}
@ -317,12 +312,9 @@ fn showDatasetInfo(allocator: std.mem.Allocator, name: []const u8, options: *con
}
if (std.mem.startsWith(u8, response, "ERROR") or std.mem.startsWith(u8, response, "NOT_FOUND")) {
colors.printError("Dataset '{s}' not found.\n", .{name});
std.debug.print("Dataset '{s}' not found.\n", .{name});
} else {
colors.printInfo("Dataset Information:\n", .{});
colors.printInfo("===================\n", .{});
colors.printSuccess("Name: {s}\n", .{name});
colors.printSuccess("Details: {s}\n", .{response});
std.debug.print("{s}\n", .{response});
}
}
@ -344,7 +336,7 @@ fn searchDatasets(allocator: std.mem.Allocator, term: []const u8, options: *cons
const formatted = std.fmt.bufPrint(&buffer, "{{\"ok\":true,\"action\":\"search\",\"validated\":true,\"term\":\"{s}\"}}\n", .{term}) catch unreachable;
try stdout_file.writeAll(formatted);
} else {
colors.printInfo("Validation OK\n", .{});
std.debug.print("Validation OK\n", .{});
}
return;
}
@ -369,18 +361,15 @@ fn searchDatasets(allocator: std.mem.Allocator, term: []const u8, options: *cons
return;
}
colors.printInfo("Search Results for '{s}':\n", .{term});
colors.printInfo("========================\n\n", .{});
if (std.mem.eql(u8, response, "[]")) {
colors.printWarning("No datasets found matching '{s}'.\n", .{term});
std.debug.print("No datasets found matching '{s}'.\n", .{term});
} else {
colors.printSuccess("{s}\n", .{response});
std.debug.print("{s}\n", .{response});
}
}
fn verifyDataset(allocator: std.mem.Allocator, target: []const u8, options: *const DatasetOptions) !void {
colors.printInfo("Verifying dataset: {s}\n", .{target});
std.debug.print("Verifying dataset: {s}\n", .{target});
const path = if (std.fs.path.isAbsolute(target))
target
@ -389,7 +378,7 @@ fn verifyDataset(allocator: std.mem.Allocator, target: []const u8, options: *con
defer if (!std.fs.path.isAbsolute(target)) allocator.free(path);
var dir = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch {
colors.printError("Dataset not found: {s}\n", .{target});
std.debug.print("Dataset not found: {s}\n", .{target});
return error.FileNotFound;
};
defer dir.close();
@ -414,7 +403,7 @@ fn verifyDataset(allocator: std.mem.Allocator, target: []const u8, options: *con
// Compute native SHA256 hash
const hash = blk: {
break :blk native_hash.hashDirectory(allocator, path) catch |err| {
colors.printWarning("Hash computation failed: {s}\n", .{@errorName(err)});
std.debug.print("Hash computation failed: {s}\n", .{@errorName(err)});
// Continue without hash - verification still succeeded
break :blk null;
};
@ -446,41 +435,39 @@ fn verifyDataset(allocator: std.mem.Allocator, target: []const u8, options: *con
try stdout_file.writeAll(line5);
}
} else {
colors.printSuccess("✓ Dataset verified\n", .{});
colors.printInfo(" Path: {s}\n", .{target});
colors.printInfo(" Files: {d}\n", .{file_count});
colors.printInfo(" Size: {d:.2} MB\n", .{@as(f64, @floatFromInt(total_size)) / (1024 * 1024)});
std.debug.print("Dataset verified\n", .{});
std.debug.print("{s}\t{d}\t{d:.2} MB\n", .{ target, file_count, @as(f64, @floatFromInt(total_size)) / (1024 * 1024) });
if (hash) |h| {
colors.printInfo(" SHA256: {s}\n", .{h});
std.debug.print("SHA256\t{s}\n", .{h});
}
}
}
fn hashDataset(allocator: std.mem.Allocator, path: []const u8) !void {
colors.printInfo("Computing native SHA256 hash for: {s}\n", .{path});
std.debug.print("Computing native SHA256 hash for: {s}\n", .{path});
// Check SIMD availability
if (!native_hash.hasSimdSha256()) {
colors.printWarning("SIMD SHA256 not available, using generic implementation\n", .{});
std.debug.print("SIMD SHA256 not available, using generic implementation\n", .{});
} else {
const impl_name = native_hash.getSimdImplName();
colors.printInfo("Using {s} SHA256 implementation\n", .{impl_name});
std.debug.print("Using {s} SHA256 implementation\n", .{impl_name});
}
// Compute hash using native library
const hash = native_hash.hashDirectory(allocator, path) catch |err| {
switch (err) {
error.ContextInitFailed => {
colors.printError("Failed to initialize native hash context\n", .{});
std.debug.print("Failed to initialize native hash context\n", .{});
},
error.HashFailed => {
colors.printError("Hash computation failed\n", .{});
std.debug.print("Hash computation failed\n", .{});
},
error.InvalidPath => {
colors.printError("Invalid path: {s}\n", .{path});
std.debug.print("Invalid path: {s}\n", .{path});
},
error.OutOfMemory => {
colors.printError("Out of memory\n", .{});
std.debug.print("Out of memory\n", .{});
},
}
return err;
@ -488,7 +475,7 @@ fn hashDataset(allocator: std.mem.Allocator, path: []const u8) !void {
defer allocator.free(hash);
// Print result
colors.printSuccess("SHA256: {s}\n", .{hash});
std.debug.print("SHA256: {s}\n", .{hash});
}
fn writeJSONString(writer: anytype, s: []const u8) !void {

View file

@ -2,7 +2,6 @@ const std = @import("std");
const config = @import("../config.zig");
const db = @import("../db.zig");
const core = @import("../core.zig");
const colors = @import("../utils/colors.zig");
const mode = @import("../mode.zig");
const uuid = @import("../utils/uuid.zig");
const crypto = @import("../utils/crypto.zig");
@ -35,7 +34,7 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
var command_args = try core.flags.parseCommon(allocator, args, &flags);
defer command_args.deinit(allocator);
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
if (flags.help or command_args.items.len == 0) {
return printUsage();
@ -51,9 +50,7 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, subcommand, "show")) {
return try showExperiment(allocator, sub_args, flags.json);
} else {
const msg = try std.fmt.allocPrint(allocator, "Unknown subcommand: {s}", .{subcommand});
defer allocator.free(msg);
core.output.errorMsg("experiment", msg);
core.output.err("Unknown subcommand");
return printUsage();
}
}
@ -74,7 +71,7 @@ fn createExperiment(allocator: std.mem.Allocator, args: []const []const u8, json
}
if (name == null) {
core.output.errorMsg("experiment", "--name is required");
core.output.err("--name is required");
return error.MissingArgument;
}
@ -124,7 +121,7 @@ fn createExperiment(allocator: std.mem.Allocator, args: []const []const u8, json
if (json) {
std.debug.print("{{\"success\":true,\"experiment_id\":\"{s}\",\"name\":\"{s}\"}}\n", .{ exp_id, name.? });
} else {
colors.printSuccess("Created experiment: {s} ({s})\n", .{ name.?, exp_id[0..8] });
std.debug.print("Created experiment: {s} ({s})\n", .{ name.?, exp_id[0..8] });
}
} else {
// Server mode: send to server via WebSocket
@ -159,10 +156,10 @@ fn createExperiment(allocator: std.mem.Allocator, args: []const []const u8, json
if (json) {
std.debug.print("{{\"success\":true,\"name\":\"{s}\",\"source\":\"server\"}}\n", .{name.?});
} else {
colors.printSuccess("Created experiment on server: {s}\n", .{name.?});
std.debug.print("Created experiment on server: {s}\n", .{name.?});
}
} else {
colors.printError("Failed to create experiment on server: {s}\n", .{response});
std.debug.print("Failed to create experiment on server: {s}\n", .{response});
return error.ServerError;
}
}
@ -215,15 +212,11 @@ fn listExperiments(allocator: std.mem.Allocator, _: []const []const u8, json: bo
std.debug.print("]\n", .{});
} else {
if (experiments.items.len == 0) {
colors.printInfo("No experiments found.\n", .{});
std.debug.print("No experiments found.\n", .{});
} else {
colors.printInfo("Experiments:\n", .{});
for (experiments.items) |e| {
const sync_indicator = if (e.synced) "" else "";
std.debug.print(" {s} {s} {s} ({s})\n", .{ sync_indicator, e.id[0..8], e.name, e.status });
if (e.description.len > 0) {
std.debug.print(" {s}\n", .{e.description});
}
const sync_indicator = if (e.synced) "S" else "U";
std.debug.print("{s}\t{s}\t{s}\t{s}\n", .{ sync_indicator, e.id[0..8], e.name, e.status });
}
}
}
@ -248,7 +241,6 @@ fn listExperiments(allocator: std.mem.Allocator, _: []const []const u8, json: bo
if (json) {
std.debug.print("{s}\n", .{response});
} else {
colors.printInfo("Experiments from server:\n", .{});
std.debug.print("{s}\n", .{response});
}
}
@ -256,7 +248,7 @@ fn listExperiments(allocator: std.mem.Allocator, _: []const []const u8, json: bo
fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
if (args.len == 0) {
core.output.errorMsg("experiment", "experiment_id required");
core.output.err("experiment_id required");
return error.MissingArgument;
}
@ -285,9 +277,7 @@ fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json:
try db.DB.bindText(exp_stmt, 1, exp_id);
if (!try db.DB.step(exp_stmt)) {
const msg = try std.fmt.allocPrint(allocator, "Experiment not found: {s}", .{exp_id});
defer allocator.free(msg);
core.output.errorMsg("experiment", msg);
core.output.err("Experiment not found");
return error.NotFound;
}
@ -320,17 +310,15 @@ fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json:
if (synced) "true" else "false", run_count, last_run orelse "null",
});
} else {
colors.printInfo("Experiment: {s}\n", .{name});
std.debug.print(" ID: {s}\n", .{exp_id});
std.debug.print(" Status: {s}\n", .{status});
std.debug.print("{s}\t{s}\t{s}\n", .{ name, exp_id, status });
if (description.len > 0) {
std.debug.print(" Description: {s}\n", .{description});
std.debug.print("Description\t{s}\n", .{description});
}
std.debug.print(" Created: {s}\n", .{created_at});
std.debug.print(" Synced: {s}\n", .{if (synced) "" else "↑ pending"});
std.debug.print(" Runs: {d}\n", .{run_count});
std.debug.print("Created\t{s}\n", .{created_at});
std.debug.print("Synced\t{s}\n", .{if (synced) "yes" else "no"});
std.debug.print("Runs\t{d}\n", .{run_count});
if (last_run) |lr| {
std.debug.print(" Last run: {s}\n", .{lr});
std.debug.print("LastRun\t{s}\n", .{lr});
}
}
} else {
@ -353,7 +341,6 @@ fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json:
if (json) {
std.debug.print("{s}\n", .{response});
} else {
colors.printInfo("Experiment details from server:\n", .{});
std.debug.print("{s}\n", .{response});
}
}
@ -366,15 +353,15 @@ fn generateExperimentID(allocator: std.mem.Allocator) ![]const u8 {
fn printUsage() !void {
std.debug.print("Usage: ml experiment <subcommand> [options]\n\n", .{});
std.debug.print("Subcommands:\n", .{});
std.debug.print(" create --name <name> [--description <desc>] Create new experiment\n", .{});
std.debug.print(" list List experiments\n", .{});
std.debug.print(" show <experiment_id> Show experiment details\n", .{});
std.debug.print("\tcreate --name <name> [--description <desc>]\tCreate new experiment\n", .{});
std.debug.print("\tlist\t\t\t\t\t\tList experiments\n", .{});
std.debug.print("\tshow <experiment_id>\t\t\t\tShow experiment details\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print(" --name <string> Experiment name (required for create)\n", .{});
std.debug.print(" --description <string> Experiment description\n", .{});
std.debug.print(" --help, -h Show this help\n", .{});
std.debug.print(" --json Output structured JSON\n\n", .{});
std.debug.print("\t--name <string>\t\tExperiment name (required for create)\n", .{});
std.debug.print("\t--description <string>\tExperiment description\n", .{});
std.debug.print("\t--help, -h\t\tShow this help\n", .{});
std.debug.print("\t--json\t\t\tOutput structured JSON\n\n", .{});
std.debug.print("Examples:\n", .{});
std.debug.print(" ml experiment create --name \"baseline-cnn\"\n", .{});
std.debug.print(" ml experiment list\n", .{});
std.debug.print("\tml experiment create --name \"baseline-cnn\"\n", .{});
std.debug.print("\tml experiment list\n", .{});
}

View file

@ -1,5 +1,4 @@
const std = @import("std");
const colors = @import("../utils/colors.zig");
const Config = @import("../config.zig").Config;
const crypto = @import("../utils/crypto.zig");
const io = @import("../utils/io.zig");
@ -52,18 +51,18 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
return printUsage();
} else {
core.output.errorMsg("export", "Unknown option");
core.output.err("Unknown option");
return error.InvalidArgs;
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
// Validate anonymize level
if (!std.mem.eql(u8, anonymize_level, "metadata-only") and
!std.mem.eql(u8, anonymize_level, "full"))
{
core.output.errorMsg("export", "Invalid anonymize level");
core.output.err("Invalid anonymize level");
return error.InvalidArgs;
}
@ -71,7 +70,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var stdout_writer = io.stdoutWriter();
try stdout_writer.print("{{\"success\":true,\"anonymize_level\":\"{s}\"}}\n", .{anonymize_level});
} else {
colors.printInfo("Anonymization level: {s}\n", .{anonymize_level});
std.debug.print("Anonymization level: {s}\n", .{anonymize_level});
}
const cfg = try Config.load(allocator);
@ -83,7 +82,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
const resolved_base = base_override orelse cfg.worker_base;
const manifest_path = manifest.resolvePathWithBase(allocator, target, resolved_base) catch |err| {
if (err == error.FileNotFound) {
colors.printError(
std.debug.print(
"Could not locate run_manifest.json for '{s}'.\n",
.{target},
);
@ -94,14 +93,14 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
// Read the manifest
const manifest_content = manifest.readFileAlloc(allocator, manifest_path) catch |err| {
colors.printError("Failed to read manifest: {}\n", .{err});
std.debug.print("Failed to read manifest: {}\n", .{err});
return err;
};
defer allocator.free(manifest_content);
// Parse the manifest
const parsed = std.json.parseFromSlice(std.json.Value, allocator, manifest_content, .{}) catch |err| {
colors.printError("Failed to parse manifest: {}\n", .{err});
std.debug.print("Failed to parse manifest: {}\n", .{err});
return err;
};
defer parsed.deinit();
@ -134,10 +133,10 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
anonymize,
});
} else {
colors.printSuccess("Exported to {s}\n", .{bundle_path});
std.debug.print("Exported to {s}\n", .{bundle_path});
if (anonymize) {
colors.printInfo(" Anonymization level: {s}\n", .{anonymize_level});
colors.printInfo(" Paths redacted, IPs removed, usernames anonymized\n", .{});
std.debug.print("\tAnonymization level: {s}\n", .{anonymize_level});
std.debug.print("\tPaths redacted, IPs removed, usernames anonymized\n", .{});
}
}
} else {
@ -265,23 +264,23 @@ fn anonymizePath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
}
fn printUsage() !void {
colors.printInfo("Usage: ml export <run-id|path> [options]\n", .{});
colors.printInfo("\nExport experiment for sharing or archiving:\n", .{});
colors.printInfo(" --bundle <path> Create tarball at path\n", .{});
colors.printInfo(" --anonymize Enable anonymization\n", .{});
colors.printInfo(" --anonymize-level <lvl> 'metadata-only' or 'full'\n", .{});
colors.printInfo(" --base <path> Base path to find run\n", .{});
colors.printInfo(" --json Output JSON response\n", .{});
colors.printInfo("\nAnonymization rules:\n", .{});
colors.printInfo(" - Paths: /nas/private/... → /datasets/...\n", .{});
colors.printInfo(" - Hostnames: gpu-server-01 → worker-A\n", .{});
colors.printInfo(" - IPs: 192.168.1.100 → [REDACTED]\n", .{});
colors.printInfo(" - Usernames: user@lab.edu → [REDACTED]\n", .{});
colors.printInfo(" - Full level: Also removes logs and annotations\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml export run_abc --bundle run_abc.tar.gz\n", .{});
colors.printInfo(" ml export run_abc --bundle run_abc.tar.gz --anonymize\n", .{});
colors.printInfo(" ml export run_abc --anonymize --anonymize-level full\n", .{});
std.debug.print("Usage: ml export <run-id|path> [options]\n", .{});
std.debug.print("\nExport experiment for sharing or archiving:\n", .{});
std.debug.print("\t--bundle <path>\t\tCreate tarball at path\n", .{});
std.debug.print("\t--anonymize\t\tEnable anonymization\n", .{});
std.debug.print("\t--anonymize-level <lvl>\t'metadata-only' or 'full'\n", .{});
std.debug.print("\t--base <path>\t\tBase path to find run\n", .{});
std.debug.print("\t--json\t\t\tOutput JSON response\n", .{});
std.debug.print("\nAnonymization rules:\n", .{});
std.debug.print("\t- Paths: /nas/private/... → /datasets/...\n", .{});
std.debug.print("\t- Hostnames: gpu-server-01 → worker-A\n", .{});
std.debug.print("\t- IPs: 192.168.1.100 → [REDACTED]\n", .{});
std.debug.print("\t- Usernames: user@lab.edu → [REDACTED]\n", .{});
std.debug.print("\t- Full level: Also removes logs and annotations\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print("\tml export run_abc --bundle run_abc.tar.gz\n", .{});
std.debug.print("\tml export run_abc --bundle run_abc.tar.gz --anonymize\n", .{});
std.debug.print("\tml export run_abc --anonymize --anonymize-level full\n", .{});
}
fn writeJSONValue(writer: anytype, v: std.json.Value) !void {

View file

@ -1,5 +1,4 @@
const std = @import("std");
const colors = @import("../utils/colors.zig");
const Config = @import("../config.zig").Config;
const crypto = @import("../utils/crypto.zig");
const io = @import("../utils/io.zig");
@ -81,7 +80,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
before = argv[i + 1];
i += 1;
} else {
core.output.errorMsg("find", "Unknown option");
core.output.err("Unknown option");
return error.InvalidArgs;
}
}
@ -97,7 +96,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
colors.printInfo("Searching experiments...\n", .{});
std.debug.print("Searching experiments...\n", .{});
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
@ -133,7 +132,7 @@ pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var out = io.stdoutWriter();
try out.print("{{\"error\":\"invalid_response\"}}\n", .{});
} else {
colors.printError("Failed to parse search results\n", .{});
std.debug.print("Failed to parse search results\n", .{});
}
return error.InvalidResponse;
};
@ -225,9 +224,10 @@ fn buildSearchJson(allocator: std.mem.Allocator, options: *const FindOptions) ![
return buf.toOwnedSlice(allocator);
}
fn outputHumanResults(root: std.json.Value, options: *const FindOptions) !void {
fn outputHumanResults(root: std.json.Value, _options: *const FindOptions) !void {
_ = _options;
if (root != .object) {
colors.printError("Invalid response format\n", .{});
std.debug.print("Invalid response format\n", .{});
return;
}
@ -236,37 +236,29 @@ fn outputHumanResults(root: std.json.Value, options: *const FindOptions) !void {
// Check for error
if (obj.get("error")) |err| {
if (err == .string) {
colors.printError("Search error: {s}\n", .{err.string});
std.debug.print("Search error: {s}\n", .{err.string});
}
return;
}
const results = obj.get("results") orelse obj.get("experiments") orelse obj.get("runs");
if (results == null) {
colors.printInfo("No results found\n", .{});
std.debug.print("No results found\n", .{});
return;
}
if (results.? != .array) {
colors.printError("Invalid results format\n", .{});
std.debug.print("Invalid results format\n", .{});
return;
}
const items = results.?.array.items;
if (items.len == 0) {
colors.printInfo("No experiments found matching your criteria\n", .{});
std.debug.print("No experiments found matching your criteria\n", .{});
return;
}
colors.printSuccess("Found {d} experiment(s)\n\n", .{items.len});
// Print header
colors.printInfo("{s:12} {s:20} {s:15} {s:10} {s}\n", .{
"ID", "Job Name", "Outcome", "Status", "Group/Tags",
});
colors.printInfo("{s}\n", .{"────────────────────────────────────────────────────────────────────────────────"});
for (items) |item| {
if (item != .object) continue;
const run_obj = item.object;
@ -274,72 +266,43 @@ fn outputHumanResults(root: std.json.Value, options: *const FindOptions) !void {
const id = jsonGetString(run_obj, "id") orelse jsonGetString(run_obj, "run_id") orelse "unknown";
const short_id = if (id.len > 8) id[0..8] else id;
const job_name = jsonGetString(run_obj, "job_name") orelse "unnamed";
const job_display = if (job_name.len > 18) job_name[0..18] else job_name;
const job_name = jsonGetString(run_obj, "job_name") orelse "";
const outcome = jsonGetString(run_obj, "outcome") orelse "-";
const status = jsonGetString(run_obj, "status") orelse "unknown";
// Build group/tags summary
var summary_buf: [30]u8 = undefined;
const summary = blk: {
// Build group/tags field
var group_tags_buf: [100]u8 = undefined;
const group_tags = blk: {
const group = jsonGetString(run_obj, "experiment_group");
const tags = run_obj.get("tags");
if (group) |g| {
if (tags) |t| {
if (t == .string) {
break :blk std.fmt.bufPrint(&summary_buf, "{s}/{s}", .{ g[0..@min(g.len, 10)], t.string[0..@min(t.string.len, 10)] }) catch g[0..@min(g.len, 15)];
break :blk std.fmt.bufPrint(&group_tags_buf, "{s}/{s}", .{ g, t.string }) catch g;
}
}
break :blk g[0..@min(g.len, 20)];
break :blk g;
}
break :blk "-";
if (tags) |t| {
if (t == .string) break :blk t.string;
}
break :blk "";
};
// Color code by outcome
if (std.mem.eql(u8, outcome, "validates")) {
colors.printSuccess("{s:12} {s:20} {s:15} {s:10} {s}\n", .{
short_id, job_display, outcome, status, summary,
});
} else if (std.mem.eql(u8, outcome, "refutes")) {
colors.printError("{s:12} {s:20} {s:15} {s:10} {s}\n", .{
short_id, job_display, outcome, status, summary,
});
} else if (std.mem.eql(u8, outcome, "partial") or std.mem.eql(u8, outcome, "inconclusive")) {
colors.printWarning("{s:12} {s:20} {s:15} {s:10} {s}\n", .{
short_id, job_display, outcome, status, summary,
});
} else {
colors.printInfo("{s:12} {s:20} {s:15} {s:10} {s}\n", .{
short_id, job_display, outcome, status, summary,
});
}
// Show hypothesis if available and query matches
if (options.query) |_| {
if (run_obj.get("narrative")) |narr| {
if (narr == .object) {
if (narr.object.get("hypothesis")) |h| {
if (h == .string and h.string.len > 0) {
const hypo = h.string;
const display = if (hypo.len > 50) hypo[0..50] else hypo;
colors.printInfo(" ↳ {s}...\n", .{display});
}
}
}
}
}
// TSV output: id => outcome | status | job_name | group_tags
std.debug.print("{s} => {s}\t{s}\t{s}\t{s}\n", .{
short_id, outcome, status, job_name, group_tags,
});
}
colors.printInfo("\nUse 'ml info <id>' for details, 'ml compare <a> <b>' to compare runs\n", .{});
}
fn outputCsvResults(allocator: std.mem.Allocator, root: std.json.Value, options: *const FindOptions) !void {
_ = options;
if (root != .object) {
colors.printError("Invalid response format\n", .{});
std.debug.print("Invalid response format\n", .{});
return;
}
@ -348,7 +311,7 @@ fn outputCsvResults(allocator: std.mem.Allocator, root: std.json.Value, options:
// Check for error
if (obj.get("error")) |err| {
if (err == .string) {
colors.printError("Search error: {s}\n", .{err.string});
std.debug.print("Search error: {s}\n", .{err.string});
}
return;
}
@ -486,22 +449,22 @@ fn hexDigit(v: u8) u8 {
}
fn printUsage() !void {
colors.printInfo("Usage: ml find [query] [options]\n", .{});
colors.printInfo("\nSearch experiments by:\n", .{});
colors.printInfo(" Query (free text): ml find \"hypothesis: warmup\"\n", .{});
colors.printInfo(" Tags: ml find --tag ablation\n", .{});
colors.printInfo(" Outcome: ml find --outcome validates\n", .{});
colors.printInfo(" Dataset: ml find --dataset imagenet\n", .{});
colors.printInfo(" Experiment group: ml find --experiment-group lr-scaling\n", .{});
colors.printInfo(" Author: ml find --author user@lab.edu\n", .{});
colors.printInfo(" Time range: ml find --after 2024-01-01 --before 2024-03-01\n", .{});
colors.printInfo("\nOptions:\n", .{});
colors.printInfo(" --limit <n> Max results (default: 20)\n", .{});
colors.printInfo(" --json Output as JSON\n", .{});
colors.printInfo(" --csv Output as CSV\n", .{});
colors.printInfo(" --help, -h Show this help\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml find --tag ablation --outcome validates\n", .{});
colors.printInfo(" ml find --experiment-group batch-scaling --json\n", .{});
colors.printInfo(" ml find \"learning rate\" --after 2024-01-01\n", .{});
std.debug.print("Usage: ml find [query] [options]\n", .{});
std.debug.print("\nSearch experiments by:\n", .{});
std.debug.print("\tQuery (free text):\tml find \"hypothesis: warmup\"\n", .{});
std.debug.print("\tTags:\t\t\tml find --tag ablation\n", .{});
std.debug.print("\tOutcome:\t\tml find --outcome validates\n", .{});
std.debug.print("\tDataset:\t\tml find --dataset imagenet\n", .{});
std.debug.print("\tExperiment group:\tml find --experiment-group lr-scaling\n", .{});
std.debug.print("\tAuthor:\t\t\tml find --author user@lab.edu\n", .{});
std.debug.print("\tTime range:\t\tml find --after 2024-01-01 --before 2024-03-01\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print("\t--limit <n>\tMax results (default: 20)\n", .{});
std.debug.print("\t--json\t\tOutput as JSON\n", .{});
std.debug.print("\t--csv\t\tOutput as CSV\n", .{});
std.debug.print("\t--help, -h\tShow this help\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print("\tml find --tag ablation --outcome validates\n", .{});
std.debug.print("\tml find --experiment-group batch-scaling --json\n", .{});
std.debug.print("\tml find \"learning rate\" --after 2024-01-01\n", .{});
}

View file

@ -8,7 +8,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
var remaining = try core.flags.parseCommon(allocator, args, &flags);
defer remaining.deinit(allocator);
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
// Handle help flag early
if (flags.help) {
@ -26,31 +26,31 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Print resolved config
std.debug.print("Resolved config:\n", .{});
std.debug.print(" tracking_uri = {s}", .{cfg.tracking_uri});
std.debug.print("\ttracking_uri = {s}", .{cfg.tracking_uri});
// Indicate if using default
if (cli_tracking_uri == null and std.mem.eql(u8, cfg.tracking_uri, "sqlite://./fetch_ml.db")) {
std.debug.print(" (default)\n", .{});
std.debug.print("\t(default)\n", .{});
} else {
std.debug.print("\n", .{});
}
std.debug.print(" artifact_path = {s}", .{cfg.artifact_path});
std.debug.print("\tartifact_path = {s}", .{cfg.artifact_path});
if (cli_artifact_path == null and std.mem.eql(u8, cfg.artifact_path, "./experiments/")) {
std.debug.print(" (default)\n", .{});
std.debug.print("\t(default)\n", .{});
} else {
std.debug.print("\n", .{});
}
std.debug.print(" sync_uri = {s}\n", .{if (cfg.sync_uri.len > 0) cfg.sync_uri else "(not set)"});
std.debug.print("\tsync_uri = {s}\n", .{if (cfg.sync_uri.len > 0) cfg.sync_uri else "(not set)"});
std.debug.print("\n", .{});
// Default path: create config only (no DB speculatively)
if (!force_local) {
std.debug.print("Created .fetchml/config.toml\n", .{});
std.debug.print(" Local tracking DB will be created automatically if server becomes unavailable.\n", .{});
std.debug.print("Created .fetchml/config.toml\n", .{});
std.debug.print("\tLocal tracking DB will be created automatically if server becomes unavailable.\n", .{});
if (cfg.sync_uri.len > 0) {
std.debug.print(" Server: {s}:{d}\n", .{ cfg.worker_host, cfg.worker_port });
std.debug.print("\tServer: {s}:{d}\n", .{ cfg.worker_host, cfg.worker_port });
}
return;
}
@ -71,7 +71,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
};
if (db_exists) {
std.debug.print("Database already exists: {s}\n", .{db_path});
std.debug.print("Database already exists: {s}\n", .{db_path});
} else {
// Create parent directories if needed
if (std.fs.path.dirname(db_path)) |dir| {
@ -88,22 +88,22 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
defer database.close();
defer database.checkpointOnExit();
std.debug.print("Created database: {s}\n", .{db_path});
std.debug.print("Created database: {s}\n", .{db_path});
}
std.debug.print("Created .fetchml/config.toml\n", .{});
std.debug.print("Schema applied (WAL mode enabled)\n", .{});
std.debug.print(" fetch_ml.db-wal and fetch_ml.db-shm will appear during use — expected.\n", .{});
std.debug.print(" The DB is just a file. Delete it freely — recreated automatically on next run.\n", .{});
std.debug.print("Created .fetchml/config.toml\n", .{});
std.debug.print("Schema applied (WAL mode enabled)\n", .{});
std.debug.print("\tfetch_ml.db-wal and fetch_ml.db-shm will appear during use — expected.\n", .{});
std.debug.print("\tThe DB is just a file. Delete it freely — recreated automatically on next run.\n", .{});
}
fn printUsage() void {
std.debug.print("Usage: ml init [OPTIONS]\n\n", .{});
std.debug.print("Initialize FetchML configuration\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --local Create local database now (default: config only)\n", .{});
std.debug.print(" --tracking-uri URI SQLite database path (e.g., sqlite://./fetch_ml.db)\n", .{});
std.debug.print(" --artifact-path PATH Artifacts directory (default: ./experiments/)\n", .{});
std.debug.print(" --sync-uri URI Server to sync with (e.g., wss://ml.company.com/ws)\n", .{});
std.debug.print(" -h, --help Show this help\n", .{});
std.debug.print("\t--local\t\t\tCreate local database now (default: config only)\n", .{});
std.debug.print("\t--tracking-uri URI\tSQLite database path (e.g., sqlite://./fetch_ml.db)\n", .{});
std.debug.print("\t--artifact-path PATH\tArtifacts directory (default: ./experiments/)\n", .{});
std.debug.print("\t--sync-uri URI\t\tServer to sync with (e.g., wss://ml.company.com/ws)\n", .{});
std.debug.print("\t-h, --help\t\tShow this help\n", .{});
}

View file

@ -1,5 +1,4 @@
const std = @import("std");
const colors = @import("../utils/colors.zig");
const ws = @import("../net/ws/client.zig");
const protocol = @import("../net/protocol.zig");
const crypto = @import("../utils/crypto.zig");
@ -27,7 +26,7 @@ fn validatePackageName(name: []const u8) bool {
fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = json;
if (args.len < 1) {
core.output.errorMsg("jupyter.restore", "Usage: ml jupyter restore <name>");
core.output.err("Usage: ml jupyter restore <name>");
return;
}
const name = args[0];
@ -42,7 +41,7 @@ fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json:
defer allocator.free(url);
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -50,21 +49,21 @@ fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json:
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
core.output.info("Restoring workspace {s}...", .{name});
std.debug.print("Restoring workspace {s}...", .{name});
client.sendRestoreJupyter(name, api_key_hash) catch |err| {
core.output.errorMsgDetailed("jupyter.restore", "Failed to send restore command", @errorName(err));
client.sendRestoreJupyter(name, api_key_hash) catch {
core.output.err("Failed to send restore command");
return;
};
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
@ -72,17 +71,17 @@ fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json:
switch (packet.packet_type) {
.success => {
if (packet.success_message) |msg| {
core.output.info("{s}", .{msg});
std.debug.print("{s}", .{msg});
} else {
core.output.info("Workspace restored.", .{});
std.debug.print("Workspace restored.", .{});
}
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
core.output.errorMsgDetailed("jupyter.restore", error_msg, packet.error_details orelse packet.error_message orelse "");
std.debug.print("Error: {s}\n", .{error_msg});
},
else => {
core.output.errorMsg("jupyter.restore", "Unexpected response type");
core.output.err("Unexpected response type");
},
}
}
@ -170,7 +169,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, sub, "uninstall")) {
return uninstallJupyter(allocator, args[1..]);
} else {
core.output.errorMsg("jupyter", "Unknown subcommand");
core.output.err("Unknown subcommand");
return error.InvalidArgs;
}
}
@ -178,27 +177,27 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
fn printUsage() !void {
std.debug.print("Usage: ml jupyter <command> [args]\n", .{});
std.debug.print("\nCommands:\n", .{});
std.debug.print(" list List Jupyter services\n", .{});
std.debug.print(" status Show Jupyter service status\n", .{});
std.debug.print(" launch Launch a new Jupyter service\n", .{});
std.debug.print(" terminate Terminate a Jupyter service\n", .{});
std.debug.print(" save Save workspace\n", .{});
std.debug.print(" restore Restore workspace\n", .{});
std.debug.print(" install Install packages\n", .{});
std.debug.print(" uninstall Uninstall packages\n", .{});
std.debug.print("\tlist\t\tList Jupyter services\n", .{});
std.debug.print("\tstatus\t\tShow Jupyter service status\n", .{});
std.debug.print("\tlaunch\t\tLaunch a new Jupyter service\n", .{});
std.debug.print("\tterminate\tTerminate a Jupyter service\n", .{});
std.debug.print("\tsave\t\tSave workspace\n", .{});
std.debug.print("\trestore\t\tRestore workspace\n", .{});
std.debug.print("\tinstall\t\tInstall packages\n", .{});
std.debug.print("\tuninstall\tUninstall packages\n", .{});
}
fn printUsagePackage() void {
colors.printError("Usage: ml jupyter package <action> [options]\n", .{});
core.output.info("Actions:\n", .{});
core.output.info("{s}", .{});
colors.printInfo("Options:\n", .{});
colors.printInfo(" --help, -h Show this help message\n", .{});
std.debug.print("Usage: ml jupyter package <action> [options]\n", .{});
std.debug.print("Actions:\n", .{});
std.debug.print("{s}", .{});
std.debug.print("Options:\n", .{});
std.debug.print("\t--help, -h Show this help message\n", .{});
}
fn createJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter create <name> [--path <path>] [--password <password>]\n", .{});
std.debug.print("Usage: ml jupyter create <name> [--path <path>] [--password <password>]\n", .{});
return;
}
@ -226,17 +225,17 @@ fn createJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
if (!validateWorkspacePath(workspace_path)) {
colors.printError("Invalid workspace path\n", .{});
std.debug.print("Invalid workspace path\n", .{});
return error.InvalidArgs;
}
std.fs.cwd().makePath(workspace_path) catch |err| {
colors.printError("Failed to create workspace directory: {}\n", .{err});
std.debug.print("Failed to create workspace directory: {}\n", .{err});
return;
};
var start_args = std.ArrayList([]const u8).initCapacity(allocator, 8) catch |err| {
colors.printError("Failed to allocate args: {}\n", .{err});
std.debug.print("Failed to allocate args: {}\n", .{err});
return;
};
defer start_args.deinit(allocator);
@ -284,7 +283,7 @@ fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Connect to WebSocket
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -293,53 +292,53 @@ fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
colors.printInfo("Starting Jupyter service '{s}'...\n", .{name});
std.debug.print("Starting Jupyter service '{s}'...\n", .{name});
// Send start command
client.sendStartJupyter(name, workspace, password, api_key_hash) catch |err| {
colors.printError("Failed to send start command: {}\n", .{err});
std.debug.print("Failed to send start command: {}\n", .{err});
return;
};
// Receive response
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
// Parse response packet
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.success => {
colors.printSuccess("Jupyter service started!\n", .{});
std.debug.print("Jupyter service started!\n", .{});
if (packet.success_message) |msg| {
std.debug.print("{s}\n", .{msg});
}
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
colors.printError("Failed to start service: {s}\n", .{error_msg});
std.debug.print("Failed to start service: {s}\n", .{error_msg});
if (packet.error_details) |details| {
colors.printError("Details: {s}\n", .{details});
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
colors.printError("Details: {s}\n", .{msg});
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
colors.printError("Unexpected response type\n", .{});
std.debug.print("Unexpected response type\n", .{});
},
}
}
fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter stop <service_id>\n", .{});
std.debug.print("Usage: ml jupyter stop <service_id>\n", .{});
return;
}
const service_id = args[0];
@ -355,7 +354,7 @@ fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Connect to WebSocket
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -364,50 +363,50 @@ fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
colors.printInfo("Stopping service {s}...\n", .{service_id});
std.debug.print("Stopping service {s}...\n", .{service_id});
// Send stop command
client.sendStopJupyter(service_id, api_key_hash) catch |err| {
colors.printError("Failed to send stop command: {}\n", .{err});
std.debug.print("Failed to send stop command: {}\n", .{err});
return;
};
// Receive response
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
// Parse response packet
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.success => {
colors.printSuccess("Service stopped.\n", .{});
std.debug.print("Service stopped.\n", .{});
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
colors.printError("Failed to stop service: {s}\n", .{error_msg});
std.debug.print("Failed to stop service: {s}\n", .{error_msg});
if (packet.error_details) |details| {
colors.printError("Details: {s}\n", .{details});
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
colors.printError("Details: {s}\n", .{msg});
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
colors.printError("Unexpected response type\n", .{});
std.debug.print("Unexpected response type\n", .{});
},
}
}
fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
std.debug.print("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
return;
}
@ -422,8 +421,8 @@ fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, args[i], "--force")) {
force = true;
} else {
colors.printError("Unknown option: {s}\n", .{args[i]});
colors.printError("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
std.debug.print("Unknown option: {s}\n", .{args[i]});
std.debug.print("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
return error.InvalidArgs;
}
}
@ -431,20 +430,20 @@ fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Trash-first by default: no confirmation.
// Permanent deletion requires explicit --purge and a strong confirmation unless --force.
if (purge and !force) {
colors.printWarning("PERMANENT deletion requested for '{s}'.\n", .{service_id});
colors.printWarning("This cannot be undone.\n", .{});
colors.printInfo("Type the service name to confirm: ", .{});
std.debug.print("PERMANENT deletion requested for '{s}'.\n", .{service_id});
std.debug.print("This cannot be undone.\n", .{});
std.debug.print("Type the service name to confirm: ", .{});
const stdin = std.fs.File{ .handle = @intCast(0) }; // stdin file descriptor
var buffer: [256]u8 = undefined;
const bytes_read = stdin.read(&buffer) catch |err| {
colors.printError("Failed to read input: {}\n", .{err});
std.debug.print("Failed to read input: {}\n", .{err});
return;
};
const line = buffer[0..bytes_read];
const typed = std.mem.trim(u8, line, "\n\r ");
if (!std.mem.eql(u8, typed, service_id)) {
colors.printInfo("Operation cancelled.\n", .{});
std.debug.print("Operation cancelled.\n", .{});
return;
}
}
@ -460,7 +459,7 @@ fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Connect to WebSocket
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -470,46 +469,46 @@ fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
defer allocator.free(api_key_hash);
if (purge) {
colors.printInfo("Permanently deleting service {s}...\n", .{service_id});
std.debug.print("Permanently deleting service {s}...\n", .{service_id});
} else {
colors.printInfo("Removing service {s} (move to trash)...\n", .{service_id});
std.debug.print("Removing service {s} (move to trash)...\n", .{service_id});
}
// Send remove command
client.sendRemoveJupyter(service_id, api_key_hash, purge) catch |err| {
colors.printError("Failed to send remove command: {}\n", .{err});
std.debug.print("Failed to send remove command: {}\n", .{err});
return;
};
// Receive response
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
// Parse response packet
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.success => {
colors.printSuccess("Service removed successfully.\n", .{});
std.debug.print("Service removed successfully.\n", .{});
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
colors.printError("Failed to remove service: {s}\n", .{error_msg});
std.debug.print("Failed to remove service: {s}\n", .{error_msg});
if (packet.error_details) |details| {
colors.printError("Details: {s}\n", .{details});
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
colors.printError("Details: {s}\n", .{msg});
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
colors.printError("Unexpected response type\n", .{});
std.debug.print("Unexpected response type\n", .{});
},
}
}
@ -539,7 +538,7 @@ fn listServices(allocator: std.mem.Allocator) !void {
// Connect to WebSocket
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -550,27 +549,27 @@ fn listServices(allocator: std.mem.Allocator) !void {
// Send list command
client.sendListJupyter(api_key_hash) catch |err| {
colors.printError("Failed to send list command: {}\n", .{err});
std.debug.print("Failed to send list command: {}\n", .{err});
return;
};
// Receive response
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
// Parse response packet
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.data => {
colors.printInfo("Jupyter Services:\n", .{});
std.debug.print("Jupyter Services:\n", .{});
if (packet.data_payload) |payload| {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
std.debug.print("{s}\n", .{payload});
@ -594,12 +593,12 @@ fn listServices(allocator: std.mem.Allocator) !void {
const services = services_opt.?;
if (services.items.len == 0) {
colors.printInfo("No running services.\n", .{});
std.debug.print("No running services.\n", .{});
return;
}
colors.printInfo("NAME STATUS URL WORKSPACE\n", .{});
colors.printInfo("---- ------ --- ---------\n", .{});
std.debug.print("NAME\t\t\t\t\t\t\t\t\tSTATUS\t\tURL\t\t\t\t\t\t\t\t\t\t\tWORKSPACE\n", .{});
std.debug.print("---- ------ --- ---------\n", .{});
for (services.items) |item| {
if (item != .object) continue;
@ -628,22 +627,22 @@ fn listServices(allocator: std.mem.Allocator) !void {
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
colors.printError("Failed to list services: {s}\n", .{error_msg});
std.debug.print("Failed to list services: {s}\n", .{error_msg});
if (packet.error_details) |details| {
colors.printError("Details: {s}\n", .{details});
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
colors.printError("Details: {s}\n", .{msg});
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
colors.printError("Unexpected response type\n", .{});
std.debug.print("Unexpected response type\n", .{});
},
}
}
fn workspaceCommands(args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter workspace <create|list|delete>\n", .{});
std.debug.print("Usage: ml jupyter workspace <create|list|delete>\n", .{});
return;
}
@ -651,7 +650,7 @@ fn workspaceCommands(args: []const []const u8) !void {
if (std.mem.eql(u8, subcommand, "create")) {
if (args.len < 2) {
colors.printError("Usage: ml jupyter workspace create --path <path>\n", .{});
std.debug.print("Usage: ml jupyter workspace create --path <path>\n", .{});
return;
}
@ -669,25 +668,25 @@ fn workspaceCommands(args: []const []const u8) !void {
// Security validation
if (!validateWorkspacePath(path)) {
colors.printError("Invalid workspace path: {s}\n", .{path});
colors.printError("Path must be relative and cannot contain '..' for security reasons.\n", .{});
std.debug.print("Invalid workspace path: {s}\n", .{path});
std.debug.print("Path must be relative and cannot contain '..' for security reasons.\n", .{});
return;
}
colors.printInfo("Creating workspace: {s}\n", .{path});
colors.printInfo("Security: Path validated against security policies\n", .{});
colors.printSuccess("Workspace created!\n", .{});
colors.printInfo("Note: Workspace is isolated and has restricted access.\n", .{});
std.debug.print("Creating workspace: {s}\n", .{path});
std.debug.print("Security: Path validated against security policies\n", .{});
std.debug.print("Workspace created!\n", .{});
std.debug.print("Note: Workspace is isolated and has restricted access.\n", .{});
} else if (std.mem.eql(u8, subcommand, "list")) {
colors.printInfo("Workspaces:\n", .{});
colors.printInfo("Name Path Status\n", .{});
colors.printInfo("---- ---- ------\n", .{});
colors.printInfo("default ./workspace active\n", .{});
colors.printInfo("ml_project ./ml_project inactive\n", .{});
colors.printInfo("Security: All workspaces are sandboxed and isolated.\n", .{});
std.debug.print("Workspaces:\n", .{});
std.debug.print("Name Path Status\n", .{});
std.debug.print("---- ---- ------\n", .{});
std.debug.print("default ./workspace active\n", .{});
std.debug.print("ml_project ./ml_project inactive\n", .{});
std.debug.print("Security: All workspaces are sandboxed and isolated.\n", .{});
} else if (std.mem.eql(u8, subcommand, "delete")) {
if (args.len < 2) {
colors.printError("Usage: ml jupyter workspace delete --path <path>\n", .{});
std.debug.print("Usage: ml jupyter workspace delete --path <path>\n", .{});
return;
}
@ -705,47 +704,47 @@ fn workspaceCommands(args: []const []const u8) !void {
// Security validation
if (!validateWorkspacePath(path)) {
colors.printError("Invalid workspace path: {s}\n", .{path});
colors.printError("Path must be relative and cannot contain '..' for security reasons.\n", .{});
std.debug.print("Invalid workspace path: {s}\n", .{path});
std.debug.print("Path must be relative and cannot contain '..' for security reasons.\n", .{});
return;
}
colors.printInfo("Deleting workspace: {s}\n", .{path});
colors.printInfo("Security: All data will be permanently removed.\n", .{});
colors.printSuccess("Workspace deleted!\n", .{});
std.debug.print("Deleting workspace: {s}\n", .{path});
std.debug.print("Security: All data will be permanently removed.\n", .{});
std.debug.print("Workspace deleted!\n", .{});
} else {
colors.printError("Invalid workspace command: {s}\n", .{subcommand});
std.debug.print("Invalid workspace command: {s}\n", .{subcommand});
}
}
fn experimentCommands(args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter experiment <link|queue|sync|status>\n", .{});
std.debug.print("Usage: ml jupyter experiment <link|queue|sync|status>\n", .{});
return;
}
const subcommand = args[0];
if (std.mem.eql(u8, subcommand, "link")) {
colors.printInfo("Linking workspace with experiment...\n", .{});
colors.printSuccess("Workspace linked with experiment successfully!\n", .{});
std.debug.print("Linking workspace with experiment...\n", .{});
std.debug.print("Workspace linked with experiment successfully!\n", .{});
} else if (std.mem.eql(u8, subcommand, "queue")) {
colors.printInfo("Queuing experiment from workspace...\n", .{});
colors.printSuccess("Experiment queued successfully!\n", .{});
std.debug.print("Queuing experiment from workspace...\n", .{});
std.debug.print("Experiment queued successfully!\n", .{});
} else if (std.mem.eql(u8, subcommand, "sync")) {
colors.printInfo("Syncing workspace with experiment data...\n", .{});
colors.printSuccess("Sync completed!\n", .{});
std.debug.print("Syncing workspace with experiment data...\n", .{});
std.debug.print("Sync completed!\n", .{});
} else if (std.mem.eql(u8, subcommand, "status")) {
colors.printInfo("Experiment status for workspace: ./workspace\n", .{});
colors.printInfo("Linked experiment: exp_123\n", .{});
std.debug.print("Experiment status for workspace: ./workspace\n", .{});
std.debug.print("Linked experiment: exp_123\n", .{});
} else {
colors.printError("Invalid experiment command: {s}\n", .{subcommand});
std.debug.print("Invalid experiment command: {s}\n", .{subcommand});
}
}
fn packageCommands(args: []const []const u8) !void {
if (args.len < 1) {
colors.printError("Usage: ml jupyter package <list>\n", .{});
std.debug.print("Usage: ml jupyter package <list>\n", .{});
return;
}
@ -753,7 +752,7 @@ fn packageCommands(args: []const []const u8) !void {
if (std.mem.eql(u8, subcommand, "list")) {
if (args.len < 2) {
colors.printError("Usage: ml jupyter package list <service-name>\n", .{});
std.debug.print("Usage: ml jupyter package list <service-name>\n", .{});
return;
}
@ -764,7 +763,7 @@ fn packageCommands(args: []const []const u8) !void {
service_name = args[1];
}
if (service_name.len == 0) {
colors.printError("Service name is required\n", .{});
std.debug.print("Service name is required\n", .{});
return;
}
@ -779,7 +778,7 @@ fn packageCommands(args: []const []const u8) !void {
defer allocator.free(url);
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
colors.printError("Failed to connect to server: {}\n", .{err});
std.debug.print("Failed to connect to server: {}\n", .{err});
return;
};
defer client.close();
@ -788,25 +787,25 @@ fn packageCommands(args: []const []const u8) !void {
defer allocator.free(api_key_hash);
client.sendListJupyterPackages(service_name, api_key_hash) catch |err| {
colors.printError("Failed to send list packages command: {}\n", .{err});
std.debug.print("Failed to send list packages command: {}\n", .{err});
return;
};
const response = client.receiveMessage(allocator) catch |err| {
colors.printError("Failed to receive response: {}\n", .{err});
std.debug.print("Failed to receive response: {}\n", .{err});
return;
};
defer allocator.free(response);
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
colors.printError("Failed to parse response: {}\n", .{err});
std.debug.print("Failed to parse response: {}\n", .{err});
return;
};
defer packet.deinit(allocator);
switch (packet.packet_type) {
.data => {
colors.printInfo("Installed packages for {s}:\n", .{service_name});
std.debug.print("Installed packages for {s}:\n", .{service_name});
if (packet.data_payload) |payload| {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
std.debug.print("{s}\n", .{payload});
@ -821,12 +820,12 @@ fn packageCommands(args: []const []const u8) !void {
const pkgs = parsed.value.array;
if (pkgs.items.len == 0) {
colors.printInfo("No packages found.\n", .{});
std.debug.print("No packages found.\n", .{});
return;
}
colors.printInfo("NAME VERSION SOURCE\n", .{});
colors.printInfo("---- ------- ------\n", .{});
std.debug.print("NAME VERSION SOURCE\n", .{});
std.debug.print("---- ------- ------\n", .{});
for (pkgs.items) |item| {
if (item != .object) continue;
@ -851,19 +850,19 @@ fn packageCommands(args: []const []const u8) !void {
},
.error_packet => {
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
colors.printError("Failed to list packages: {s}\n", .{error_msg});
std.debug.print("Failed to list packages: {s}\n", .{error_msg});
if (packet.error_details) |details| {
colors.printError("Details: {s}\n", .{details});
std.debug.print("Details: {s}\n", .{details});
} else if (packet.error_message) |msg| {
colors.printError("Details: {s}\n", .{msg});
std.debug.print("Details: {s}\n", .{msg});
}
},
else => {
colors.printError("Unexpected response type\n", .{});
std.debug.print("Unexpected response type\n", .{});
},
}
} else {
colors.printError("Invalid package command: {s}\n", .{subcommand});
std.debug.print("Invalid package command: {s}\n", .{subcommand});
}
}
@ -871,7 +870,7 @@ fn launchJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: b
_ = allocator;
_ = args;
_ = json;
core.output.errorMsg("jupyter.launch", "Not implemented");
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
}
@ -879,7 +878,7 @@ fn terminateJupyter(allocator: std.mem.Allocator, args: []const []const u8, json
_ = allocator;
_ = args;
_ = json;
core.output.errorMsg("jupyter.terminate", "Not implemented");
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
}
@ -887,20 +886,20 @@ fn saveJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: boo
_ = allocator;
_ = args;
_ = json;
core.output.errorMsg("jupyter.save", "Not implemented");
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
}
fn installJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
_ = allocator;
_ = args;
core.output.errorMsg("jupyter.install", "Not implemented");
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
}
fn uninstallJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
_ = allocator;
_ = args;
core.output.errorMsg("jupyter.uninstall", "Not implemented");
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const config = @import("../config.zig");
const core = @import("../core.zig");
const colors = @import("../utils/colors.zig");
const manifest_lib = @import("../manifest.zig");
const mode = @import("../mode.zig");
const ws = @import("../net/ws/client.zig");
@ -13,11 +12,12 @@ const crypto = @import("../utils/crypto.zig");
/// ml logs <run_id> # Fetch logs from local file or server
/// ml logs <run_id> --follow # Stream logs from server
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
var flags = core.flags.CommonFlags{};
var flags: core.flags.CommonFlags = .{};
var command_args = try core.flags.parseCommon(allocator, args, &flags);
defer command_args.deinit(allocator);
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
if (flags.help) {
return printUsage();
@ -148,7 +148,7 @@ fn streamServerLogs(allocator: std.mem.Allocator, target: []const u8, cfg: confi
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
colors.printInfo("Streaming logs for: {s}\n", .{target});
std.debug.print("Streaming logs for: {s}\n", .{target});
try client.sendStreamLogs(target, api_key_hash);
@ -171,7 +171,7 @@ fn streamServerLogs(allocator: std.mem.Allocator, target: []const u8, cfg: confi
},
.error_packet => {
const err_msg = packet.error_message orelse "Stream error";
colors.printError("Error: {s}\n", .{err_msg});
core.output.err(err_msg);
return error.ServerError;
},
else => {},
@ -183,10 +183,10 @@ fn printUsage() !void {
std.debug.print("Usage: ml logs <run_id> [options]\n\n", .{});
std.debug.print("Fetch or stream run logs.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --follow, -f Stream logs from server (online mode)\n", .{});
std.debug.print(" --help, -h Show this help message\n", .{});
std.debug.print(" --json Output structured JSON\n\n", .{});
std.debug.print("\t--follow, -f\tStream logs from server (online mode)\n", .{});
std.debug.print("\t--help, -h\tShow this help message\n", .{});
std.debug.print("\t--json\t\tOutput structured JSON\n\n", .{});
std.debug.print("Examples:\n", .{});
std.debug.print(" ml logs abc123 # Fetch logs (local or server)\n", .{});
std.debug.print(" ml logs abc123 --follow # Stream logs from server\n", .{});
std.debug.print("\tml logs abc123\t\t# Fetch logs (local or server)\n", .{});
std.debug.print("\tml logs abc123 --follow\t# Stream logs from server\n", .{});
}

View file

@ -26,7 +26,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
}
core.output.init(if (flags.flags.json) .flags.json else .text);
core.output.setMode(if (flags.flags.json) .flags.json else .text);
if (keep_count == null and older_than_days == null) {
core.output.usage("prune", "ml prune --keep <n> | --older-than <days>");
@ -95,13 +95,13 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (flags.json) {
std.debug.print("{\"ok\":true}\n", .{});
} else {
logging.success("Prune operation completed successfully\n", .{});
logging.success("Prune operation completed successfully\n", .{});
}
} else {
if (flags.json) {
std.debug.print("{\"ok\":false,\"error_code\":{d}}\n", .{response[0]});
} else {
logging.err(" Prune operation failed: error code {d}\n", .{response[0]});
logging.err("[FAIL] Prune operation failed: error code {d}\n", .{response[0]});
}
return error.PruneFailed;
}
@ -109,7 +109,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (flags.json) {
std.debug.print("{\"ok\":true,\"note\":\"no_response\"}\n", .{});
} else {
logging.success("Prune request sent (no response received)\n", .{});
logging.success("Prune request sent (no response received)\n", .{});
}
}
}
@ -117,8 +117,8 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
fn printUsage() void {
logging.info("Usage: ml prune [options]\n\n", .{});
logging.info("Options:\n", .{});
logging.info(" --keep <N> Keep N most recent experiments\n", .{});
logging.info(" --older-than <days> Remove experiments older than N days\n", .{});
logging.info(" --flags.json Output machine-readable JSON\n", .{});
logging.info(" --help, -h Show this help message\n", .{});
logging.info("\t--keep <N>\t\tKeep N most recent experiments\n", .{});
logging.info("\t--older-than <days>\tRemove experiments older than N days\n", .{});
logging.info("\t--json\t\t\tOutput machine-readable JSON\n", .{});
logging.info("\t--help, -h\t\tShow this help message\n", .{});
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const Config = @import("../config.zig").Config;
const ws = @import("../net/ws/client.zig");
const colors = @import("../utils/colors.zig");
const history = @import("../utils/history.zig");
const crypto = @import("../utils/crypto.zig");
const protocol = @import("../net/protocol.zig");
@ -128,7 +127,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
// If --rerun is specified, handle re-queueing
if (rerun_id) |id| {
if (mode.isOffline(mode_result.mode)) {
colors.printError("ml queue --rerun requires server connection\n", .{});
std.debug.print("ml queue --rerun requires server connection\n", .{});
return error.RequiresServer;
}
return try handleRerun(allocator, id, args, config);
@ -136,7 +135,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
// Regular queue - requires server
if (mode.isOffline(mode_result.mode)) {
colors.printError("ml queue requires server connection (use 'ml run' for local execution)\n", .{});
std.debug.print("ml queue requires server connection (use 'ml run' for local execution)\n", .{});
return error.RequiresServer;
}
@ -147,7 +146,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
fn executeQueue(allocator: std.mem.Allocator, args: []const []const u8, config: Config) !void {
// Support batch operations - multiple job names
var job_names = std.ArrayList([]const u8).initCapacity(allocator, 10) catch |err| {
colors.printError("Failed to allocate job list: {}\n", .{err});
std.debug.print("Failed to allocate job list: {}\n", .{err});
return err;
};
defer job_names.deinit(allocator);
@ -218,21 +217,21 @@ fn executeQueue(allocator: std.mem.Allocator, args: []const []const u8, config:
const commit_in = pre[i + 1];
const commit_hex = resolveCommitHexOrPrefix(allocator, config.worker_base, commit_in) catch |err| {
if (err == error.FileNotFound) {
colors.printError("No commit matches prefix: {s}\n", .{commit_in});
std.debug.print("No commit matches prefix: {s}\n", .{commit_in});
return error.InvalidArgs;
}
colors.printError("Invalid commit id\n", .{});
std.debug.print("Invalid commit id\n", .{});
return error.InvalidArgs;
};
defer allocator.free(commit_hex);
const commit_bytes = crypto.decodeHex(allocator, commit_hex) catch {
colors.printError("Invalid commit id: must be hex\n", .{});
std.debug.print("Invalid commit id: must be hex\n", .{});
return error.InvalidArgs;
};
if (commit_bytes.len != 20) {
allocator.free(commit_bytes);
colors.printError("Invalid commit id: expected 20 bytes\n", .{});
std.debug.print("Invalid commit id: expected 20 bytes\n", .{});
return error.InvalidArgs;
}
commit_id_override = commit_bytes;
@ -332,14 +331,14 @@ fn executeQueue(allocator: std.mem.Allocator, args: []const []const u8, config:
} else {
// This is a job name
job_names.append(allocator, arg) catch |err| {
colors.printError("Failed to append job: {}\n", .{err});
std.debug.print("Failed to append job: {}\n", .{err});
return err;
};
}
}
if (job_names.items.len == 0) {
colors.printError("No job names specified\n", .{});
std.debug.print("No job names specified\n", .{});
return error.InvalidArgs;
}
@ -361,29 +360,24 @@ fn executeQueue(allocator: std.mem.Allocator, args: []const []const u8, config:
return;
}
colors.printInfo("Queueing {d} job(s)...\n", .{job_names.items.len});
std.debug.print("Queueing {d} job(s)...\n", .{job_names.items.len});
// Generate tracking JSON if needed (simplified for now)
var tracking_json: []const u8 = "";
if (has_tracking) {
tracking_json = "{}"; // Placeholder for tracking JSON
}
const tracking_json: []const u8 = "";
// Process each job
var success_count: usize = 0;
var failed_jobs = std.ArrayList([]const u8).initCapacity(allocator, 10) catch |err| {
colors.printError("Failed to allocate failed jobs list: {}\n", .{err});
std.debug.print("Failed to allocate failed jobs list: {}\n", .{err});
return err;
};
defer failed_jobs.deinit(allocator);
defer if (commit_id_override) |cid| allocator.free(cid);
const args_str: []const u8 = if (args_override) |a| a else args_joined;
const note_str: []const u8 = if (note_override) |n| n else "";
for (job_names.items, 0..) |job_name, index| {
colors.printInfo("Processing job {d}/{d}: {s}\n", .{ index + 1, job_names.items.len, job_name });
std.debug.print("Processing job {d}/{d}: {s}\n", .{ index + 1, job_names.items.len, job_name });
queueSingleJob(
allocator,
@ -398,31 +392,31 @@ fn executeQueue(allocator: std.mem.Allocator, args: []const []const u8, config:
note_str,
print_next_steps,
) catch |err| {
colors.printError("Failed to queue job '{s}': {}\n", .{ job_name, err });
std.debug.print("Failed to queue job '{s}': {}\n", .{ job_name, err });
failed_jobs.append(allocator, job_name) catch |append_err| {
colors.printError("Failed to track failed job: {}\n", .{append_err});
std.debug.print("Failed to track failed job: {}\n", .{append_err});
};
continue;
};
colors.printSuccess("Successfully queued job '{s}'\n", .{job_name});
std.debug.print("Successfully queued job '{s}'\n", .{job_name});
success_count += 1;
}
// Show summary
colors.printInfo("Batch queuing complete.\n", .{});
colors.printSuccess("Successfully queued: {d} job(s)\n", .{success_count});
std.debug.print("Batch queuing complete.\n", .{});
std.debug.print("Successfully queued: {d} job(s)\n", .{success_count});
if (failed_jobs.items.len > 0) {
colors.printError("Failed to queue: {d} job(s)\n", .{failed_jobs.items.len});
std.debug.print("Failed to queue: {d} job(s)\n", .{failed_jobs.items.len});
for (failed_jobs.items) |failed_job| {
colors.printError(" - {s}\n", .{failed_job});
std.debug.print(" - {s}\n", .{failed_job});
}
}
if (!options.json and success_count > 0 and job_names.items.len > 1) {
colors.printInfo("\nNext steps:\n", .{});
colors.printInfo(" ml status --watch\n", .{});
std.debug.print("\nNext steps:\n", .{});
std.debug.print(" ml status --watch\n", .{});
}
}
@ -448,9 +442,9 @@ fn handleRerun(allocator: std.mem.Allocator, run_id: []const u8, args: []const [
// Parse response (simplified)
if (std.mem.indexOf(u8, message, "success") != null) {
colors.printSuccess("Re-queued run {s}\n", .{run_id[0..8]});
std.debug.print("Re-queued run {s}\n", .{run_id[0..8]});
} else {
colors.printError("Failed to re-queue: {s}\n", .{message});
std.debug.print("Failed to re-queue: {s}\n", .{message});
return error.RerunFailed;
}
}
@ -508,7 +502,7 @@ fn queueSingleJob(
}
}
colors.printInfo("Queueing job '{s}' with commit {s}...\n", .{ job_name, commit_hex });
std.debug.print("Queueing job '{s}' with commit {s}...\n", .{ job_name, commit_hex });
// Connect to WebSocket and send queue message
const ws_url = try config.getWebSocketUrl(allocator);
@ -518,11 +512,11 @@ fn queueSingleJob(
defer client.close();
if ((snapshot_id != null) != (snapshot_sha256 != null)) {
colors.printError("Both --snapshot-id and --snapshot-sha256 must be set\n", .{});
std.debug.print("Both --snapshot-id and --snapshot-sha256 must be set\n", .{});
return error.InvalidArgs;
}
if (snapshot_id != null and tracking_json.len > 0) {
colors.printError("Snapshot queueing is not supported with tracking yet\n", .{});
std.debug.print("Snapshot queueing is not supported with tracking yet\n", .{});
return error.InvalidArgs;
}
@ -633,7 +627,7 @@ fn queueSingleJob(
if (message.len > 0 and message[0] == '{') {
try handleDuplicateResponse(allocator, message, job_name, commit_hex, options);
} else {
colors.printInfo("Server response: {s}\n", .{message});
std.debug.print("Server response: {s}\n", .{message});
}
return;
};
@ -642,97 +636,85 @@ fn queueSingleJob(
switch (packet.packet_type) {
.success => {
history.record(allocator, job_name, commit_hex) catch |err| {
colors.printWarning("Warning: failed to record job in history ({})", .{err});
std.debug.print("Warning: failed to record job in history ({})\n", .{err});
};
if (options.json) {
std.debug.print("{{\"success\":true,\"job_name\":\"{s}\",\"commit_id\":\"{s}\",\"status\":\"queued\"}}\n", .{ job_name, commit_hex });
} else {
colors.printSuccess("✓ Job queued successfully: {s}\n", .{job_name});
std.debug.print("Job queued: {s}\n", .{job_name});
if (print_next_steps) {
const next_steps = try formatNextSteps(allocator, job_name, commit_hex);
defer allocator.free(next_steps);
colors.printInfo("\n{s}", .{next_steps});
std.debug.print("{s}\n", .{next_steps});
}
}
},
.data => {
if (packet.data_payload) |payload| {
try handleDuplicateResponse(allocator, payload, job_name, commit_hex, options);
}
},
.error_packet => {
const err_msg = packet.error_message orelse "Unknown error";
if (options.json) {
std.debug.print("{{\"success\":false,\"error\":\"{s}\"}}\n", .{err_msg});
} else {
colors.printError("Error: {s}\n", .{err_msg});
std.debug.print("Error: {s}\n", .{err_msg});
}
return error.ServerError;
},
else => {
try client.handleResponsePacket(packet, "Job queue");
history.record(allocator, job_name, commit_hex) catch |err| {
colors.printWarning("Warning: failed to record job in history ({})", .{err});
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);
defer allocator.free(next_steps);
colors.printInfo("\n{s}", .{next_steps});
std.debug.print("{s}\n", .{next_steps});
}
},
}
}
fn printUsage() !void {
colors.printInfo("Usage: ml queue <job-name> [job-name ...] [options]\n", .{});
colors.printInfo(" ml queue --rerun <run_id> # Re-queue a completed run\n", .{});
colors.printInfo("\nBasic Options:\n", .{});
colors.printInfo(" --commit <id> Specify commit ID\n", .{});
colors.printInfo(" --priority <num> Set priority (0-255, default: 5)\n", .{});
colors.printInfo(" --help, -h Show this help message\n", .{});
colors.printInfo(" --cpu <cores> CPU cores requested (default: 2)\n", .{});
colors.printInfo(" --memory <gb> Memory in GB (default: 8)\n", .{});
colors.printInfo(" --gpu <count> GPU count (default: 0)\n", .{});
colors.printInfo(" --gpu-memory <gb> GPU memory budget (default: auto)\n", .{});
colors.printInfo(" --args <string> Extra runner args (sent to worker as task.Args)\n", .{});
colors.printInfo(" --note <string> Human notes (stored in run manifest as metadata.note)\n", .{});
colors.printInfo(" -- <args...> Extra runner args (alternative to --args)\n", .{});
colors.printInfo("\nResearch Narrative:\n", .{});
colors.printInfo(" --hypothesis <text> Research hypothesis being tested\n", .{});
colors.printInfo(" --context <text> Background context for this experiment\n", .{});
colors.printInfo(" --intent <text> What you're trying to accomplish\n", .{});
colors.printInfo(" --expected-outcome <text> What you expect to happen\n", .{});
colors.printInfo(" --experiment-group <name> Group related experiments\n", .{});
colors.printInfo(" --tags <csv> Comma-separated tags (e.g., ablation,batch-size)\n", .{});
colors.printInfo("\nSpecial Modes:\n", .{});
colors.printInfo(" --rerun <run_id> Re-queue a completed local run to server\n", .{});
colors.printInfo(" --dry-run Show what would be queued\n", .{});
colors.printInfo(" --validate Validate experiment without queuing\n", .{});
colors.printInfo(" --explain Explain what will happen\n", .{});
colors.printInfo(" --json Output structured JSON\n", .{});
colors.printInfo(" --force Queue even if duplicate exists\n", .{});
colors.printInfo("\nTracking:\n", .{});
colors.printInfo(" --mlflow Enable MLflow (sidecar)\n", .{});
colors.printInfo(" --mlflow-uri <uri> Enable MLflow (remote)\n", .{});
colors.printInfo(" --tensorboard Enable TensorBoard\n", .{});
colors.printInfo(" --wandb-key <key> Enable Wandb with API key\n", .{});
colors.printInfo(" --wandb-project <prj> Set Wandb project\n", .{});
colors.printInfo(" --wandb-entity <ent> Set Wandb entity\n", .{});
colors.printInfo("\nSandboxing:\n", .{});
colors.printInfo(" --network <mode> Network mode: none, bridge, slirp4netns\n", .{});
colors.printInfo(" --read-only Mount root filesystem as read-only\n", .{});
colors.printInfo(" --secret <name> Inject secret as env var (can repeat)\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml queue my_job # Queue a job\n", .{});
colors.printInfo(" ml queue my_job --dry-run # Preview submission\n", .{});
colors.printInfo(" ml queue my_job --validate # Validate locally\n", .{});
colors.printInfo(" ml queue --rerun abc123 # Re-queue completed run\n", .{});
colors.printInfo(" ml status --watch # Watch queue + prewarm\n", .{});
colors.printInfo("\nResearch Examples:\n", .{});
colors.printInfo(" ml queue train.py --hypothesis 'LR scaling improves convergence' \n", .{});
colors.printInfo(" --context 'Following paper XYZ' --tags ablation,lr-scaling\n", .{});
std.debug.print("Usage: ml queue [options] <job_name> [job_name2 ...]\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print("\t--priority <1-10>\tJob priority (default: 5)\n", .{});
std.debug.print("\t--commit <hex>\t\tSpecific commit to run\n", .{});
std.debug.print("\t--snapshot-id <id>\tSnapshot ID to use\n", .{});
std.debug.print("\t--snapshot-sha256 <sha>\tSnapshot SHA256 to use\n", .{});
std.debug.print("\t--dry-run\t\tShow what would be queued\n", .{});
std.debug.print("\t--explain <reason>\tReason for running\n", .{});
std.debug.print("\t--json\t\t\tOutput machine-readable JSON\n", .{});
std.debug.print("\t--help, -h\t\tShow this help message\n", .{});
std.debug.print("\t--context <text>\tBackground context for this experiment\n", .{});
std.debug.print("\t--intent <text>\t\tWhat you're trying to accomplish\n", .{});
std.debug.print("\t--expected-outcome <text>\tWhat you expect to happen\n", .{});
std.debug.print("\t--experiment-group <name>\tGroup related experiments\n", .{});
std.debug.print("\t--tags <csv>\t\tComma-separated tags (e.g., ablation,batch-size)\n", .{});
std.debug.print("\nSpecial Modes:\n", .{});
std.debug.print("\t--rerun <run_id>\tRe-queue a completed local run to server\n", .{});
std.debug.print("\t--dry-run\t\tShow what would be queued\n", .{});
std.debug.print("\t--validate\t\tValidate experiment without queuing\n", .{});
std.debug.print("\t--explain\t\tExplain what will happen\n", .{});
std.debug.print("\t--json\t\t\tOutput structured JSON\n", .{});
std.debug.print("\t--force\t\t\tQueue even if duplicate exists\n", .{});
std.debug.print("\nTracking:\n", .{});
std.debug.print("\t--mlflow\t\tEnable MLflow (sidecar)\n", .{});
std.debug.print("\t--mlflow-uri <uri>\tEnable MLflow (remote)\n", .{});
std.debug.print("\t--tensorboard\t\tEnable TensorBoard\n", .{});
std.debug.print("\t--wandb-key <key>\tEnable Wandb with API key\n", .{});
std.debug.print("\t--wandb-project <prj>\tSet Wandb project\n", .{});
std.debug.print("\t--wandb-entity <ent>\tSet Wandb entity\n", .{});
std.debug.print("\nSandboxing:\n", .{});
std.debug.print("\t--network <mode>\tNetwork mode: none, bridge, slirp4netns\n", .{});
std.debug.print("\t--read-only\t\tMount root filesystem as read-only\n", .{});
std.debug.print("\t--secret <name>\t\tInject secret as env var (can repeat)\n", .{});
std.debug.print("\nExamples:\n", .{});
std.debug.print("\tml queue my_job\t\t\t # Queue a job\n", .{});
std.debug.print("\tml queue my_job --dry-run\t # Preview submission\n", .{});
std.debug.print("\tml queue my_job --validate\t # Validate locally\n", .{});
std.debug.print("\tml queue --rerun abc123\t # Re-queue completed run\n", .{});
std.debug.print("\tml status --watch\t\t # Watch queue + prewarm\n", .{});
std.debug.print("\nResearch Examples:\n", .{});
std.debug.print("\tml queue train.py --hypothesis 'LR scaling improves convergence'\n", .{});
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 {
@ -741,9 +723,9 @@ pub fn formatNextSteps(allocator: std.mem.Allocator, job_name: []const u8, commi
const writer = out.writer(allocator);
try writer.writeAll("Next steps:\n");
try writer.writeAll(" ml status --watch\n");
try writer.print(" ml cancel {s}\n", .{job_name});
try writer.print(" ml validate {s}\n", .{commit_hex});
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);
}
@ -783,40 +765,40 @@ fn explainJob(
}
return;
} else {
colors.printInfo("Job Explanation:\n", .{});
colors.printInfo(" Job Name: {s}\n", .{job_name});
colors.printInfo(" Commit ID: {s}\n", .{commit_display});
colors.printInfo(" Priority: {d}\n", .{priority});
colors.printInfo(" Resources Requested:\n", .{});
colors.printInfo(" CPU: {d} cores\n", .{options.cpu});
colors.printInfo(" Memory: {d} GB\n", .{options.memory});
colors.printInfo(" GPU: {d} device(s)\n", .{options.gpu});
colors.printInfo(" GPU Memory: {s}\n", .{options.gpu_memory orelse "auto"});
std.debug.print("Job Explanation:\n", .{});
std.debug.print("\tJob Name: {s}\n", .{job_name});
std.debug.print("\tCommit ID: {s}\n", .{commit_display});
std.debug.print("\tPriority: {d}\n", .{priority});
std.debug.print("\tResources Requested:\n", .{});
std.debug.print("\t\tCPU: {d} cores\n", .{options.cpu});
std.debug.print("\t\tMemory: {d} GB\n", .{options.memory});
std.debug.print("\t\tGPU: {d} device(s)\n", .{options.gpu});
std.debug.print("\t\tGPU Memory: {s}\n", .{options.gpu_memory orelse "auto"});
// Display narrative if provided
if (narrative_json != null) {
colors.printInfo("\n Research Narrative:\n", .{});
std.debug.print("\n\tResearch Narrative:\n", .{});
if (options.hypothesis) |h| {
colors.printInfo(" Hypothesis: {s}\n", .{h});
std.debug.print("\t\tHypothesis: {s}\n", .{h});
}
if (options.context) |c| {
colors.printInfo(" Context: {s}\n", .{c});
std.debug.print("\t\tContext: {s}\n", .{c});
}
if (options.intent) |i| {
colors.printInfo(" Intent: {s}\n", .{i});
std.debug.print("\t\tIntent: {s}\n", .{i});
}
if (options.expected_outcome) |eo| {
colors.printInfo(" Expected Outcome: {s}\n", .{eo});
std.debug.print("\t\tExpected Outcome: {s}\n", .{eo});
}
if (options.experiment_group) |eg| {
colors.printInfo(" Experiment Group: {s}\n", .{eg});
std.debug.print("\t\tExperiment Group: {s}\n", .{eg});
}
if (options.tags) |t| {
colors.printInfo(" Tags: {s}\n", .{t});
std.debug.print("\t\tTags: {s}\n", .{t});
}
}
colors.printInfo("\n Action: Job would be queued for execution\n", .{});
std.debug.print("\n Action: Job would be queued for execution\n", .{});
}
}
@ -855,20 +837,20 @@ fn validateJob(
try stdout_file.writeAll(formatted);
return;
} else {
colors.printInfo("Validation Results:\n", .{});
colors.printInfo(" Job Name: {s}\n", .{job_name});
colors.printInfo(" Commit ID: {s}\n", .{commit_display});
std.debug.print("Validation Results:\n", .{});
std.debug.print("\tJob Name: {s}\n", .{job_name});
std.debug.print("\tCommit ID: {s}\n", .{commit_display});
colors.printInfo(" Required Files:\n", .{});
const train_status = if (train_script_exists) "" else "";
const req_status = if (requirements_exists) "" else "";
colors.printInfo(" train.py {s}\n", .{train_status});
colors.printInfo(" requirements.txt {s}\n", .{req_status});
std.debug.print("\tRequired Files:\n", .{});
const train_status = if (train_script_exists) "yes" else "no";
const req_status = if (requirements_exists) "yes" else "no";
std.debug.print("\t\ttrain.py {s}\n", .{train_status});
std.debug.print("\t\trequirements.txt {s}\n", .{req_status});
if (overall_valid) {
colors.printSuccess("Validation passed - job is ready to queue\n", .{});
std.debug.print("\tValidation passed - job is ready to queue\n", .{});
} else {
colors.printError("Validation failed - missing required files\n", .{});
std.debug.print("\tValidation failed - missing required files\n", .{});
}
}
}
@ -908,42 +890,42 @@ fn dryRunJob(
}
return;
} else {
colors.printInfo("Dry Run - Job Queue Preview:\n", .{});
colors.printInfo(" Job Name: {s}\n", .{job_name});
colors.printInfo(" Commit ID: {s}\n", .{commit_display});
colors.printInfo(" Priority: {d}\n", .{priority});
colors.printInfo(" Resources Requested:\n", .{});
colors.printInfo(" CPU: {d} cores\n", .{options.cpu});
colors.printInfo(" Memory: {d} GB\n", .{options.memory});
colors.printInfo(" GPU: {d} device(s)\n", .{options.gpu});
colors.printInfo(" GPU Memory: {s}\n", .{options.gpu_memory orelse "auto"});
std.debug.print("Dry Run - Job Queue Preview:\n", .{});
std.debug.print("\tJob Name: {s}\n", .{job_name});
std.debug.print("\tCommit ID: {s}\n", .{commit_display});
std.debug.print("\tPriority: {d}\n", .{priority});
std.debug.print("\tResources Requested:\n", .{});
std.debug.print("\t\tCPU: {d} cores\n", .{options.cpu});
std.debug.print("\t\tMemory: {d} GB\n", .{options.memory});
std.debug.print("\t\tGPU: {d} device(s)\n", .{options.gpu});
std.debug.print("\t\tGPU Memory: {s}\n", .{options.gpu_memory orelse "auto"});
// Display narrative if provided
if (narrative_json != null) {
colors.printInfo("\n Research Narrative:\n", .{});
std.debug.print("\n\tResearch Narrative:\n", .{});
if (options.hypothesis) |h| {
colors.printInfo(" Hypothesis: {s}\n", .{h});
std.debug.print("\t\tHypothesis: {s}\n", .{h});
}
if (options.context) |c| {
colors.printInfo(" Context: {s}\n", .{c});
std.debug.print("\t\tContext: {s}\n", .{c});
}
if (options.intent) |i| {
colors.printInfo(" Intent: {s}\n", .{i});
std.debug.print("\t\tIntent: {s}\n", .{i});
}
if (options.expected_outcome) |eo| {
colors.printInfo(" Expected Outcome: {s}\n", .{eo});
std.debug.print("\t\tExpected Outcome: {s}\n", .{eo});
}
if (options.experiment_group) |eg| {
colors.printInfo(" Experiment Group: {s}\n", .{eg});
std.debug.print("\t\tExperiment Group: {s}\n", .{eg});
}
if (options.tags) |t| {
colors.printInfo(" Tags: {s}\n", .{t});
std.debug.print("\t\tTags: {s}\n", .{t});
}
}
colors.printInfo("\n Action: Would queue job\n", .{});
colors.printInfo(" Estimated queue time: 2-5 minutes\n", .{});
colors.printSuccess("Dry run completed - no job was actually queued\n", .{});
std.debug.print("\n\tAction: Would queue job\n", .{});
std.debug.print("\tEstimated queue time: 2-5 minutes\n", .{});
std.debug.print("\tDry run completed - no job was actually queued\n", .{});
}
}
@ -994,7 +976,7 @@ fn handleDuplicateResponse(
if (options.json) {
std.debug.print("{s}\n", .{payload});
} else {
colors.printInfo("Server response: {s}\n", .{payload});
std.debug.print("Server response: {s}\n", .{payload});
}
return;
};
@ -1006,7 +988,7 @@ fn handleDuplicateResponse(
if (options.json) {
std.debug.print("{s}\n", .{payload});
} else {
colors.printSuccess("Job queued: {s}\n", .{job_name});
std.debug.print("Job queued: {s}\n", .{job_name});
}
return;
}
@ -1022,11 +1004,11 @@ fn handleDuplicateResponse(
if (options.json) {
std.debug.print("{{\"success\":true,\"duplicate\":true,\"existing_id\":\"{s}\",\"status\":\"{s}\",\"queued_by\":\"{s}\",\"minutes_ago\":{d},\"suggested_action\":\"watch\"}}\n", .{ existing_id, status, queued_by, minutes_ago });
} else {
colors.printInfo("\nIdentical job already in progress: {s}\n", .{existing_id[0..8]});
colors.printInfo(" Queued by {s}, {d} minutes ago\n", .{ queued_by, minutes_ago });
colors.printInfo(" Status: {s}\n", .{status});
colors.printInfo("\n Watch: ml watch {s}\n", .{existing_id[0..8]});
colors.printInfo(" Rerun: ml queue {s} --commit {s} --force\n", .{ job_name, commit_hex });
std.debug.print("\nIdentical job already in progress: {s}\n", .{existing_id[0..8]});
std.debug.print("\tQueued by {s}, {d} minutes ago\n", .{ queued_by, minutes_ago });
std.debug.print("\tStatus: {s}\n", .{status});
std.debug.print("\n\tWatch: ml watch {s}\n", .{existing_id[0..8]});
std.debug.print("\tRerun: ml queue {s} --commit {s} --force\n", .{ job_name, commit_hex });
}
} else if (std.mem.eql(u8, status, "completed")) {
const duration_sec = root.get("duration_seconds").?.integer;
@ -1034,23 +1016,23 @@ fn handleDuplicateResponse(
if (options.json) {
std.debug.print("{{\"success\":true,\"duplicate\":true,\"existing_id\":\"{s}\",\"status\":\"completed\",\"queued_by\":\"{s}\",\"duration_minutes\":{d},\"suggested_action\":\"show\"}}\n", .{ existing_id, queued_by, duration_min });
} else {
colors.printInfo("\nIdentical job already completed: {s}\n", .{existing_id[0..8]});
colors.printInfo(" Queued by {s}\n", .{queued_by});
std.debug.print("\nIdentical job already completed: {s}\n", .{existing_id[0..8]});
std.debug.print(" Queued by {s}\n", .{queued_by});
const metrics = root.get("metrics");
if (metrics) |m| {
if (m == .object) {
colors.printInfo("\n Results:\n", .{});
std.debug.print("\n Results:\n", .{});
if (m.object.get("accuracy")) |v| {
if (v == .float) colors.printInfo(" accuracy: {d:.3}\n", .{v.float});
if (v == .float) std.debug.print(" accuracy: {d:.3}\n", .{v.float});
}
if (m.object.get("loss")) |v| {
if (v == .float) colors.printInfo(" loss: {d:.3}\n", .{v.float});
if (v == .float) std.debug.print(" loss: {d:.3}\n", .{v.float});
}
}
}
colors.printInfo(" duration: {d}m\n", .{duration_min});
colors.printInfo("\n Inspect: ml experiment show {s}\n", .{existing_id[0..8]});
colors.printInfo(" Rerun: ml queue {s} --commit {s} --force\n", .{ job_name, commit_hex });
std.debug.print("\t\tduration: {d}m\n", .{duration_min});
std.debug.print("\n\tInspect: ml experiment show {s}\n", .{existing_id[0..8]});
std.debug.print("\tRerun: ml queue {s} --commit {s} --force\n", .{ job_name, commit_hex });
}
} else if (std.mem.eql(u8, status, "failed")) {
const error_reason = root.get("error_reason").?.string;
@ -1069,85 +1051,85 @@ fn handleDuplicateResponse(
std.debug.print("{{\"success\":true,\"duplicate\":true,\"existing_id\":\"{s}\",\"status\":\"failed\",\"failure_class\":\"{s}\",\"exit_code\":{d},\"signal\":\"{s}\",\"error_reason\":\"{s}\",\"retry_count\":{d},\"retry_cap\":{d},\"auto_retryable\":{},\"requires_fix\":{},\"suggested_action\":\"{s}\"}}\n", .{ existing_id, failure_class, exit_code, signal, error_reason, retry_count, retry_cap, auto_retryable, requires_fix, suggested_action });
} else {
// Print rich failure information based on FailureClass
colors.printWarning("\nFAILED {s} {s} failure\n", .{ existing_id[0..8], failure_class });
std.debug.print("\nFAILED {s} {s} failure\n", .{ existing_id[0..8], failure_class });
if (signal.len > 0) {
colors.printInfo(" Signal: {s} (exit code: {d})\n", .{ signal, exit_code });
std.debug.print("\tSignal: {s} (exit code: {d})\n", .{ signal, exit_code });
} else if (exit_code != 0) {
colors.printInfo(" Exit code: {d}\n", .{exit_code});
std.debug.print("\tExit code: {d}\n", .{exit_code});
}
// Show log tail if available
if (log_tail.len > 0) {
// Truncate long log tails
const display_tail = if (log_tail.len > 160) log_tail[0..160] else log_tail;
colors.printInfo(" Log: {s}...\n", .{display_tail});
std.debug.print("\tLog: {s}...\n", .{display_tail});
}
// Show retry history
if (retry_count > 0) {
if (auto_retryable and retry_count < retry_cap) {
colors.printInfo(" Retried: {d}/{d} — auto-retry in progress\n", .{ retry_count, retry_cap });
std.debug.print("\tRetried: {d}/{d} — auto-retry in progress\n", .{ retry_count, retry_cap });
} else {
colors.printInfo(" Retried: {d}/{d}\n", .{ retry_count, retry_cap });
std.debug.print("\tRetried: {d}/{d}\n", .{ retry_count, retry_cap });
}
}
// Class-specific guidance per design spec
if (std.mem.eql(u8, failure_class, "infrastructure")) {
colors.printInfo("\n Infrastructure failure (node died, preempted).\n", .{});
std.debug.print("\n\tInfrastructure failure (node died, preempted).\n", .{});
if (auto_retryable and retry_count < retry_cap) {
colors.printSuccess(" Auto-retrying transparently (attempt {d}/{d})\n", .{ retry_count + 1, retry_cap });
std.debug.print("\t-> Auto-retrying transparently (attempt {d}/{d})\n", .{ retry_count + 1, retry_cap });
} else if (retry_count >= retry_cap) {
colors.printError(" Retry cap reached. Requires manual intervention.\n", .{});
colors.printInfo(" Resubmit: ml requeue {s}\n", .{existing_id[0..8]});
std.debug.print("\t-> Retry cap reached. Requires manual intervention.\n", .{});
std.debug.print("\tResubmit: ml requeue {s}\n", .{existing_id[0..8]});
}
colors.printInfo(" Logs: ml logs {s}\n", .{existing_id[0..8]});
std.debug.print("\tLogs: ml logs {s}\n", .{existing_id[0..8]});
} else if (std.mem.eql(u8, failure_class, "code")) {
// CRITICAL RULE: code failures never auto-retry
colors.printError("\n Code failure — auto-retry is blocked.\n", .{});
colors.printWarning(" You must fix the code before resubmitting.\n", .{});
colors.printInfo(" View logs: ml logs {s}\n", .{existing_id[0..8]});
colors.printInfo("\n After fix:\n", .{});
colors.printInfo(" Requeue with same config:\n", .{});
colors.printInfo(" ml requeue {s}\n", .{existing_id[0..8]});
colors.printInfo(" Or with more resources:\n", .{});
colors.printInfo(" ml requeue {s} --gpu-memory 16\n", .{existing_id[0..8]});
std.debug.print("\n\tCode failure — auto-retry is blocked.\n", .{});
std.debug.print("\tYou must fix the code before resubmitting.\n", .{});
std.debug.print("\t\tView logs: ml logs {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tAfter fix:\n", .{});
std.debug.print("\t\tRequeue with same config:\n", .{});
std.debug.print("\t\t\tml requeue {s}\n", .{existing_id[0..8]});
std.debug.print("\t\tOr with more resources:\n", .{});
std.debug.print("\t\t\tml requeue {s} --gpu-memory 16\n", .{existing_id[0..8]});
} else if (std.mem.eql(u8, failure_class, "data")) {
// Data failures never auto-retry
colors.printError("\n Data failure — verification/checksum issue.\n", .{});
colors.printWarning(" Auto-retry will fail again with same data.\n", .{});
colors.printInfo("\n Check:\n", .{});
colors.printInfo(" Dataset availability: ml dataset verify {s}\n", .{existing_id[0..8]});
colors.printInfo(" View logs: ml logs {s}\n", .{existing_id[0..8]});
colors.printInfo("\n After data issue resolved:\n", .{});
colors.printInfo(" ml requeue {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tData failure — verification/checksum issue.\n", .{});
std.debug.print("\tAuto-retry will fail again with same data.\n", .{});
std.debug.print("\n\tCheck:\n", .{});
std.debug.print("\t\tDataset availability: ml dataset verify {s}\n", .{existing_id[0..8]});
std.debug.print("\t\tView logs: ml logs {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tAfter data issue resolved:\n", .{});
std.debug.print("\t\t\tml requeue {s}\n", .{existing_id[0..8]});
} else if (std.mem.eql(u8, failure_class, "resource")) {
colors.printError("\n Resource failure — OOM or disk full.\n", .{});
std.debug.print("\n\tResource failure — OOM or disk full.\n", .{});
if (retry_count == 0 and auto_retryable) {
colors.printInfo(" Will retry once with backoff (30s delay)\n", .{});
std.debug.print("\t-> Will retry once with backoff (30s delay)\n", .{});
} else if (retry_count >= 1) {
colors.printWarning(" Retried once, failed again with same error.\n", .{});
colors.printInfo("\n Suggestion: resubmit with more resources:\n", .{});
colors.printInfo(" ml requeue {s} --gpu-memory 16\n", .{existing_id[0..8]});
colors.printInfo(" ml requeue {s} --memory 32 --cpu 8\n", .{existing_id[0..8]});
std.debug.print("\t-> Retried once, failed again with same error.\n", .{});
std.debug.print("\n\tSuggestion: resubmit with more resources:\n", .{});
std.debug.print("\t\tml requeue {s} --gpu-memory 16\n", .{existing_id[0..8]});
std.debug.print("\t\tml requeue {s} --memory 32 --cpu 8\n", .{existing_id[0..8]});
}
colors.printInfo("\n Check capacity: ml status\n", .{});
colors.printInfo(" Logs: ml logs {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tCheck capacity: ml status\n", .{});
std.debug.print("\tLogs: ml logs {s}\n", .{existing_id[0..8]});
} else {
// Unknown failures
colors.printWarning("\n Unknown failure — classification unclear.\n", .{});
colors.printInfo("\n Review full logs and decide:\n", .{});
colors.printInfo(" ml logs {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tUnknown failure — classification unclear.\n", .{});
std.debug.print("\n\tReview full logs and decide:\n", .{});
std.debug.print("\t\tml logs {s}\n", .{existing_id[0..8]});
if (auto_retryable) {
colors.printInfo("\n Or retry:\n", .{});
colors.printInfo(" ml requeue {s}\n", .{existing_id[0..8]});
std.debug.print("\n\tOr retry:\n", .{});
std.debug.print("\t\tml requeue {s}\n", .{existing_id[0..8]});
}
}
// Always show the suggestion if available
if (suggestion.len > 0) {
colors.printInfo("\n {s}\n", .{suggestion});
std.debug.print("\n\t{s}\n", .{suggestion});
}
}
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const db = @import("../db.zig");
const manifest_lib = @import("../manifest.zig");
const colors = @import("../utils/colors.zig");
const core = @import("../core.zig");
const config = @import("../config.zig");
@ -36,7 +35,7 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
var command_args = try core.flags.parseCommon(allocator, args, &flags);
defer command_args.deinit(allocator);
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
if (flags.help) {
return printUsage();
@ -165,9 +164,9 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
exit_code,
});
} else {
colors.printSuccess(" Run {s} complete ({s})\n", .{ run_id[0..8], status });
std.debug.print("[OK] Run {s} complete ({s})\n", .{ run_id[0..8], status });
if (cfg.sync_uri.len > 0) {
colors.printInfo(" queued for sync\n", .{});
std.debug.print("-> queued for sync\n", .{});
}
}
}
@ -413,13 +412,13 @@ fn parseAndLogMetric(
fn printUsage() !void {
std.debug.print("Usage: ml run [options] [args...]\n", .{});
std.debug.print(" ml run -- <command> [args...]\n\n", .{});
std.debug.print("\t\t\tml run -- <command> [args...]\n\n", .{});
std.debug.print("Execute a run locally with experiment tracking.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --help, -h Show this help message\n", .{});
std.debug.print(" --json Output structured JSON\n\n", .{});
std.debug.print("\t--help, -h\tShow this help message\n", .{});
std.debug.print("\t--json\t\tOutput structured JSON\n\n", .{});
std.debug.print("Examples:\n", .{});
std.debug.print(" ml run # Use entrypoint from config\n", .{});
std.debug.print(" ml run --lr 0.001 # Append args to entrypoint\n", .{});
std.debug.print(" ml run -- python train.py # Run explicit command\n", .{});
std.debug.print("\tml run\t\t\t# Use entrypoint from config\n", .{});
std.debug.print("\tml run --lr 0.001\t\t# Append args to entrypoint\n", .{});
std.debug.print("\tml run -- python train.py\t# Run explicit command\n", .{});
}

View file

@ -36,7 +36,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
}
core.output.init(if (options.json) .json else .text);
core.output.setMode(if (options.json) .json else .text);
const config = try Config.load(allocator);
defer {
@ -79,17 +79,17 @@ fn runSingleStatus(allocator: std.mem.Allocator, config: Config, user_context: a
}
fn runWatchMode(allocator: std.mem.Allocator, config: Config, user_context: auth.UserContext, options: StatusOptions) !void {
core.output.info("Starting watch mode (interval: {d}s). Press Ctrl+C to stop.\n", .{options.watch_interval});
std.debug.print("Starting watch mode (interval: {d}s). Press Ctrl+C to stop.\n", .{options.watch_interval});
while (true) {
if (!options.json) {
core.output.info("\n=== FetchML Status - {s} ===", .{user_context.name});
std.debug.print("\n=== FetchML Status - {s} ===", .{user_context.name});
}
try runSingleStatus(allocator, config, user_context, options);
if (!options.json) {
colors.printInfo("Next update in {d} seconds...\n", .{options.watch_interval});
std.debug.print("Next update in {d} seconds...\n", .{options.watch_interval});
}
std.Thread.sleep(options.watch_interval * std.time.ns_per_s);
@ -98,7 +98,7 @@ fn runWatchMode(allocator: std.mem.Allocator, config: Config, user_context: auth
fn runTuiMode(allocator: std.mem.Allocator, config: Config, args: []const []const u8) !void {
if (config.isLocalMode()) {
core.output.errorMsg("status", "TUI mode requires server mode. Use 'ml status' without --tui for local mode.");
core.output.err("TUI mode requires server mode. Use 'ml status' without --tui for local mode.");
return error.ServerOnlyFeature;
}
@ -140,12 +140,12 @@ fn runTuiMode(allocator: std.mem.Allocator, config: Config, args: []const []cons
}
fn printUsage() !void {
colors.printInfo("Usage: ml status [options]\n", .{});
colors.printInfo("\nOptions:\n", .{});
colors.printInfo(" --json Output structured JSON\n", .{});
colors.printInfo(" --watch Watch mode - continuously update status\n", .{});
colors.printInfo(" --tui Launch TUI monitor via SSH\n", .{});
colors.printInfo(" --limit <count> Limit number of results shown\n", .{});
colors.printInfo(" --watch-interval=<s> Set watch interval in seconds (default: 5)\n", .{});
colors.printInfo(" --help Show this help message\n", .{});
std.debug.print("Usage: ml status [options]\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print("\t--json\t\t\tOutput structured JSON\n", .{});
std.debug.print("\t--watch\t\t\tWatch mode - continuously update status\n", .{});
std.debug.print("\t--tui\t\t\tLaunch TUI monitor via SSH\n", .{});
std.debug.print("\t--limit <count>\tLimit number of results shown\n", .{});
std.debug.print("\t--watch-interval=<s>\tSet watch interval in seconds (default: 5)\n", .{});
std.debug.print("\t--help\t\t\tShow this help message\n", .{});
}

View file

@ -1,5 +1,4 @@
const std = @import("std");
const colors = @import("../utils/colors.zig");
const config = @import("../config.zig");
const db = @import("../db.zig");
const ws = @import("../net/ws/client.zig");
@ -22,7 +21,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
const cfg = try config.Config.load(allocator);
defer {
@ -32,7 +31,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
const mode_result = try mode.detect(allocator, cfg);
if (mode.isOffline(mode_result.mode)) {
colors.printError("ml sync requires server connection\n", .{});
std.debug.print("ml sync requires server connection\n", .{});
return error.RequiresServer;
}
@ -56,7 +55,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (try db.DB.step(stmt)) {
try runs_to_sync.append(allocator, try RunInfo.fromStmt(stmt, allocator));
} else {
colors.printWarning("Run {s} already synced or not found\n", .{run_id});
std.debug.print("Run {s} already synced or not found\n", .{run_id});
return;
}
} else {
@ -69,7 +68,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
if (runs_to_sync.items.len == 0) {
if (!flags.json) colors.printSuccess("All runs already synced!\n", .{});
if (!flags.json) std.debug.print("All runs already synced!\n", .{});
return;
}
@ -84,9 +83,9 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
var success_count: usize = 0;
for (runs_to_sync.items) |run_info| {
if (!flags.json) colors.printInfo("Syncing run {s}...\n", .{run_info.run_id[0..8]});
if (!flags.json) std.debug.print("Syncing run {s}...\n", .{run_info.run_id[0..8]});
syncRun(allocator, &database, &client, run_info, api_key_hash) catch |err| {
if (!flags.json) colors.printError("Failed to sync run {s}: {}\n", .{ run_info.run_id[0..8], err });
if (!flags.json) std.debug.print("Failed to sync run {s}: {}\n", .{ run_info.run_id[0..8], err });
continue;
};
const update_sql = "UPDATE ml_runs SET synced = 1 WHERE run_id = ?;";
@ -102,7 +101,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (flags.json) {
std.debug.print("{{\"success\":true,\"synced\":{d},\"total\":{d}}}\n", .{ success_count, runs_to_sync.items.len });
} else {
colors.printSuccess("Synced {d}/{d} runs\n", .{ success_count, runs_to_sync.items.len });
std.debug.print("Synced {d}/{d} runs\n", .{ success_count, runs_to_sync.items.len });
}
}
@ -251,11 +250,11 @@ fn printUsage() void {
std.debug.print("Usage: ml sync [run_id] [options]\n\n", .{});
std.debug.print("Push local experiment runs to the server.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --json Output structured JSON\n", .{});
std.debug.print(" --help, -h Show this help message\n\n", .{});
std.debug.print("\t--json\t\tOutput structured JSON\n", .{});
std.debug.print("\t--help, -h\tShow this help message\n\n", .{});
std.debug.print("Examples:\n", .{});
std.debug.print(" ml sync # Sync all unsynced runs\n", .{});
std.debug.print(" ml sync abc123 # Sync specific run\n", .{});
std.debug.print("\tml sync\t\t\t# Sync all unsynced runs\n", .{});
std.debug.print("\tml sync abc123\t\t# Sync specific run\n", .{});
}
/// Find the git root directory by walking up from the given path

View file

@ -3,7 +3,6 @@ const testing = std.testing;
const Config = @import("../config.zig").Config;
const ws = @import("../net/ws/client.zig");
const protocol = @import("../net/protocol.zig");
const colors = @import("../utils/colors.zig");
const crypto = @import("../utils/crypto.zig");
const io = @import("../utils/io.zig");
const core = @import("../core.zig");
@ -32,14 +31,14 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.startsWith(u8, arg, "--help")) {
return printUsage();
} else if (std.mem.startsWith(u8, arg, "--")) {
core.output.errorMsg("validate", "Unknown option");
core.output.err("Unknown option");
return error.InvalidArgs;
} else {
commit_hex = arg;
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
const config = try Config.load(allocator);
defer {
@ -62,10 +61,10 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
try client.sendValidateRequestTask(api_key_hash, tid);
} else {
if (commit_hex == null) {
core.output.errorMsg("validate", "No commit hash specified");
core.output.err("No commit hash specified");
return printUsage();
} else if (commit_hex.?.len != 40) {
colors.printError("validate requires a 40-char commit id (or --task <task_id>)\n", .{});
std.debug.print("validate requires a 40-char commit id (or --task <task_id>)\n", .{});
try printUsage();
return error.InvalidArgs;
}
@ -80,12 +79,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
defer allocator.free(msg);
const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch {
if (flags.json) {
var out = io.stdoutWriter();
try out.print("{s}\n", .{msg});
} else {
std.debug.print("{s}\n", .{msg});
}
std.debug.print("{s}\n", .{msg});
return error.InvalidPacket;
};
defer packet.deinit(allocator);
@ -96,166 +90,96 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
if (packet.packet_type != .data or packet.data_payload == null) {
colors.printError("unexpected response for validate\n", .{});
std.debug.print("unexpected response for validate\n", .{});
return error.InvalidPacket;
}
const payload = packet.data_payload.?;
if (flags.json) {
var out = io.stdoutWriter();
try out.print("{s}\n", .{payload});
} else {
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{});
defer parsed.deinit();
const root = parsed.value.object;
const ok = try printHumanReport(root, flags.verbose);
if (!ok) return error.ValidationFailed;
}
}
fn printHumanReport(root: std.json.ObjectMap, verbose: bool) !bool {
const ok_val = root.get("ok") orelse return error.InvalidPacket;
if (ok_val != .bool) return error.InvalidPacket;
const ok = ok_val.bool;
if (root.get("commit_id")) |cid| {
if (cid != .null) {
std.debug.print("commit_id: {s}\n", .{cid.string});
}
}
if (root.get("task_id")) |tid| {
if (tid != .null) {
std.debug.print("task_id: {s}\n", .{tid.string});
}
}
if (ok) {
std.debug.print("validate: OK\n", .{});
} else {
std.debug.print("validate: FAILED\n", .{});
}
if (root.get("errors")) |errs| {
if (errs == .array and errs.array.items.len > 0) {
std.debug.print("errors:\n", .{});
for (errs.array.items) |e| {
if (e == .string) {
std.debug.print("- {s}\n", .{e.string});
}
}
}
}
if (root.get("warnings")) |warns| {
if (warns == .array and warns.array.items.len > 0) {
std.debug.print("warnings:\n", .{});
for (warns.array.items) |w| {
if (w == .string) {
std.debug.print("- {s}\n", .{w.string});
}
}
}
}
if (root.get("checks")) |checks_val| {
if (checks_val == .object) {
if (verbose) {
std.debug.print("checks:\n", .{});
} else {
std.debug.print("failed_checks:\n", .{});
}
var it = checks_val.object.iterator();
var any_failed: bool = false;
while (it.next()) |entry| {
const name = entry.key_ptr.*;
const check_val = entry.value_ptr.*;
if (check_val != .object) continue;
const check_obj = check_val.object;
var check_ok: bool = false;
if (check_obj.get("ok")) |cok| {
if (cok == .bool) check_ok = cok.bool;
}
if (!check_ok) any_failed = true;
if (!verbose and check_ok) continue;
if (check_ok) {
std.debug.print("- {s}: OK\n", .{name});
} else {
std.debug.print("- {s}: FAILED\n", .{name});
}
if (verbose or !check_ok) {
if (check_obj.get("expected")) |exp| {
if (exp != .null) {
std.debug.print(" expected: {s}\n", .{exp.string});
}
}
if (check_obj.get("actual")) |act| {
if (act != .null) {
std.debug.print(" actual: {s}\n", .{act.string});
}
}
if (check_obj.get("details")) |det| {
if (det != .null) {
std.debug.print(" details: {s}\n", .{det.string});
}
}
}
}
if (!verbose and !any_failed) {
std.debug.print("- none\n", .{});
}
}
}
return ok;
}
fn printUsage() !void {
colors.printInfo("Usage:\n", .{});
std.debug.print(" ml validate <commit_id> [--json] [--verbose]\n", .{});
std.debug.print(" ml validate --task <task_id> [--json] [--verbose]\n", .{});
}
test "validate human report formatting" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const payload =
\\{
\\ "ok": false,
\\ "commit_id": "abc",
\\ "task_id": "t1",
\\ "checks": {
\\ "a": {"ok": true},
\\ "b": {"ok": false, "expected": "x", "actual": "y", "details": "d"}
\\ },
\\ "errors": ["e1"],
\\ "warnings": ["w1"],
\\ "ts": "now"
\\}
;
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{});
defer parsed.deinit();
var buf = std.ArrayList(u8).empty;
defer buf.deinit(allocator);
if (flags.json) {
try io.stdoutWriteJson(parsed.value);
} else {
const root = parsed.value.object;
const ok_val = root.get("ok") orelse return error.InvalidPacket;
if (ok_val != .bool) return error.InvalidPacket;
_ = ok_val.bool;
_ = try printHumanReport(buf.writer(), parsed.value.object, false);
try testing.expect(std.mem.indexOf(u8, buf.items, "failed_checks") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- b: FAILED") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "expected: x") != null);
if (root.get("commit_id")) |cid| {
if (cid != .null) {
std.debug.print("commit_id: {s}\n", .{cid.string});
}
}
if (root.get("task_id")) |tid| {
if (tid != .null) {
std.debug.print("task_id: {s}\n", .{tid.string});
}
}
buf.clearRetainingCapacity();
_ = try printHumanReport(buf.writer(), parsed.value.object, true);
try testing.expect(std.mem.indexOf(u8, buf.items, "checks") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- a: OK") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- b: FAILED") != null);
if (root.get("checks")) |checks_val| {
if (checks_val == .object) {
if (flags.verbose) {
std.debug.print("checks:\n", .{});
} else {
std.debug.print("failed_checks:\n", .{});
}
var it = checks_val.object.iterator();
var any_failed: bool = false;
while (it.next()) |entry| {
const name = entry.key_ptr.*;
const check_val = entry.value_ptr.*;
if (check_val != .object) continue;
const check_obj = check_val.object;
var check_ok: bool = false;
if (check_obj.get("ok")) |cok| {
if (cok == .bool) check_ok = cok.bool;
}
if (!check_ok) any_failed = true;
if (!flags.verbose and check_ok) continue;
if (check_ok) {
std.debug.print("- {s}: OK\n", .{name});
} else {
std.debug.print("- {s}: FAILED\n", .{name});
}
if (flags.verbose or !check_ok) {
if (check_obj.get("expected")) |exp| {
if (exp != .null) {
std.debug.print(" expected: {s}\n", .{exp.string});
}
}
if (check_obj.get("actual")) |act| {
if (act != .null) {
std.debug.print(" actual: {s}\n", .{act.string});
}
}
if (check_obj.get("details")) |det| {
if (det != .null) {
std.debug.print(" details: {s}\n", .{det.string});
}
}
}
}
if (!flags.verbose and !any_failed) {
std.debug.print("- none\n", .{});
}
}
}
return;
}
return;
}
fn printUsage() !void {
std.debug.print("Usage:\n", .{});
std.debug.print("\tml validate <commit_id> [--json] [--verbose]\n", .{});
std.debug.print("\tml validate --task <task_id> [--json] [--verbose]\n", .{});
}

View file

@ -5,7 +5,6 @@ const rsync = @import("../utils/rsync_embedded.zig");
const ws = @import("../net/ws/client.zig");
const core = @import("../core.zig");
const mode = @import("../mode.zig");
const colors = @import("../utils/colors.zig");
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
var flags = core.flags.CommonFlags{};
@ -27,7 +26,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
}
core.output.init(if (flags.json) .json else .text);
core.output.setMode(if (flags.json) .json else .text);
const cfg = try config.Config.load(allocator);
defer {
@ -39,7 +38,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (should_sync) {
const mode_result = try mode.detect(allocator, cfg);
if (mode.isOffline(mode_result.mode)) {
colors.printError("ml watch --sync requires server connection\n", .{});
std.debug.print("ml watch --sync requires server connection\n", .{});
return error.RequiresServer;
}
}
@ -48,11 +47,11 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
std.debug.print("{{\"ok\":true,\"action\":\"watch\",\"sync\":{s}}}\n", .{if (should_sync) "true" else "false"});
} else {
if (should_sync) {
colors.printInfo("Watching for changes with auto-sync every {d}s...\n", .{sync_interval});
std.debug.print("Watching for changes with auto-sync every {d}s...\n", .{sync_interval});
} else {
colors.printInfo("Watching directory for changes...\n", .{});
std.debug.print("Watching directory for changes...\n", .{});
}
colors.printInfo("Press Ctrl+C to stop\n", .{});
std.debug.print("Press Ctrl+C to stop\n", .{});
}
// Watch loop
@ -65,7 +64,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
const sync_cmd = @import("sync.zig");
sync_cmd.run(allocator, &[_][]const u8{"--json"}) catch |err| {
if (!flags.json) {
colors.printError("Auto-sync failed: {}\n", .{err});
std.debug.print("Auto-sync failed: {}\n", .{err});
}
};
last_synced = now;
@ -109,7 +108,7 @@ fn syncAndQueue(allocator: std.mem.Allocator, path: []const u8, job_name: ?[]con
defer allocator.free(response);
if (response.len > 0 and response[0] == 0x00) {
std.debug.print("Job queued successfully: {s}\n", .{actual_job_name});
std.debug.print("Job queued successfully: {s}\n", .{actual_job_name});
}
}
@ -120,7 +119,7 @@ fn printUsage() void {
std.debug.print("Usage: ml watch [options]\n\n", .{});
std.debug.print("Watch for changes and optionally auto-sync.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" --sync Auto-sync runs to server every 30s\n", .{});
std.debug.print(" --json Output structured JSON\n", .{});
std.debug.print(" --help, -h Show this help message\n", .{});
std.debug.print("\t--sync\t\tAuto-sync runs to server every 30s\n", .{});
std.debug.print("\t--json\t\tOutput structured JSON\n", .{});
std.debug.print("\t--help, -h\tShow this help message\n", .{});
}