const std = @import("std"); const config = @import("../config.zig"); const db = @import("../db.zig"); const core = @import("../core.zig"); const mode = @import("../mode.zig"); const uuid = @import("../utils/uuid.zig"); const crypto = @import("../utils/crypto.zig"); const ws = @import("../net/ws/client.zig"); const ExperimentInfo = struct { id: []const u8, name: []const u8, description: []const u8, created_at: []const u8, status: []const u8, synced: bool, fn deinit(self: *ExperimentInfo, allocator: std.mem.Allocator) void { allocator.free(self.id); allocator.free(self.name); allocator.free(self.description); allocator.free(self.created_at); allocator.free(self.status); } }; pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { var flags = core.flags.CommonFlags{}; var command_args = try core.flags.parseCommon(allocator, args, &flags); defer command_args.deinit(allocator); core.output.setMode(if (flags.json) .json else .text); if (flags.help or command_args.items.len == 0) { return printUsage(); } const subcommand = command_args.items[0]; const sub_args = if (command_args.items.len > 1) command_args.items[1..] else &[_][]const u8{}; if (std.mem.eql(u8, subcommand, "create")) { return try createExperiment(allocator, sub_args, flags.json); } else if (std.mem.eql(u8, subcommand, "list")) { return try listExperiments(allocator, sub_args, flags.json); } else if (std.mem.eql(u8, subcommand, "show")) { return try showExperiment(allocator, sub_args, flags.json); } else { core.output.err("Unknown subcommand"); return printUsage(); } } fn createExperiment(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void { var name: ?[]const u8 = null; var description: ?[]const u8 = null; var i: usize = 0; while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) { name = args[i + 1]; i += 1; } else if (std.mem.eql(u8, args[i], "--description") and i + 1 < args.len) { description = args[i + 1]; i += 1; } } if (name == null) { core.output.err("--name is required"); return error.MissingArgument; } const cfg = try config.Config.load(allocator); defer { var mut_cfg = cfg; mut_cfg.deinit(allocator); } // Check mode const mode_result = try mode.detect(allocator, cfg); if (mode.isOffline(mode_result.mode)) { // Local mode: create in SQLite const db_path = try cfg.getDBPath(allocator); defer allocator.free(db_path); var database = try db.DB.init(allocator, db_path); defer database.close(); // Insert experiment with synced=0 (not synced to server yet) const sql = "INSERT INTO ml_experiments (experiment_id, name, description, status, synced) VALUES (?, ?, ?, 'active', 0);"; const stmt = try database.prepare(sql); defer db.DB.finalize(stmt); const exp_id = try generateExperimentID(allocator); defer allocator.free(exp_id); try db.DB.bindText(stmt, 1, exp_id); try db.DB.bindText(stmt, 2, name.?); try db.DB.bindText(stmt, 3, description orelse ""); _ = try db.DB.step(stmt); // Update config with new experiment var mut_cfg = cfg; if (mut_cfg.experiment == null) { mut_cfg.experiment = config.ExperimentConfig{ .name = "", .entrypoint = "", }; } mut_cfg.experiment.?.name = try allocator.dupe(u8, name.?); try mut_cfg.save(allocator); database.checkpointOnExit(); if (json) { std.debug.print("{{\"success\":true,\"experiment_id\":\"{s}\",\"name\":\"{s}\"}}\n", .{ exp_id, name.? }); } else { std.debug.print("Created experiment: {s} ({s})\n", .{ name.?, exp_id[0..8] }); } } else { // Server mode: send to server via WebSocket const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); defer allocator.free(api_key_hash); const ws_url = try cfg.getWebSocketUrl(allocator); defer allocator.free(ws_url); var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); defer client.close(); try client.sendCreateExperiment(api_key_hash, name.?, description orelse ""); // Receive response const response = try client.receiveMessage(allocator); defer allocator.free(response); // Parse response (expecting JSON with experiment_id) if (std.mem.indexOf(u8, response, "experiment_id") != null) { // Also update local config var mut_cfg = cfg; if (mut_cfg.experiment == null) { mut_cfg.experiment = config.ExperimentConfig{ .name = "", .entrypoint = "", }; } mut_cfg.experiment.?.name = try allocator.dupe(u8, name.?); try mut_cfg.save(allocator); if (json) { std.debug.print("{{\"success\":true,\"name\":\"{s}\",\"source\":\"server\"}}\n", .{name.?}); } else { std.debug.print("Created experiment on server: {s}\n", .{name.?}); } } else { std.debug.print("Failed to create experiment on server: {s}\n", .{response}); return error.ServerError; } } } fn listExperiments(allocator: std.mem.Allocator, _: []const []const u8, json: bool) !void { const cfg = try config.Config.load(allocator); defer { var mut_cfg = cfg; mut_cfg.deinit(allocator); } const mode_result = try mode.detect(allocator, cfg); if (mode.isOffline(mode_result.mode)) { // Local mode: list from SQLite const db_path = try cfg.getDBPath(allocator); defer allocator.free(db_path); var database = try db.DB.init(allocator, db_path); defer database.close(); const sql = "SELECT experiment_id, name, description, created_at, status, synced FROM ml_experiments ORDER BY created_at DESC;"; const stmt = try database.prepare(sql); defer db.DB.finalize(stmt); var experiments = try std.ArrayList(ExperimentInfo).initCapacity(allocator, 16); defer { for (experiments.items) |*e| e.deinit(allocator); experiments.deinit(allocator); } while (try db.DB.step(stmt)) { try experiments.append(allocator, ExperimentInfo{ .id = try allocator.dupe(u8, db.DB.columnText(stmt, 0)), .name = try allocator.dupe(u8, db.DB.columnText(stmt, 1)), .description = try allocator.dupe(u8, db.DB.columnText(stmt, 2)), .created_at = try allocator.dupe(u8, db.DB.columnText(stmt, 3)), .status = try allocator.dupe(u8, db.DB.columnText(stmt, 4)), .synced = db.DB.columnInt64(stmt, 5) != 0, }); } if (json) { std.debug.print("[", .{}); for (experiments.items, 0..) |e, i| { if (i > 0) std.debug.print(",", .{}); std.debug.print("{{\"id\":\"{s}\",\"name\":\"{s}\",\"status\":\"{s}\",\"description\":\"{s}\",\"synced\":{s}}}", .{ e.id, e.name, e.status, e.description, if (e.synced) "true" else "false" }); } std.debug.print("]\n", .{}); } else { if (experiments.items.len == 0) { std.debug.print("No experiments found.\n", .{}); } else { for (experiments.items) |e| { 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 }); } } } } else { // Server mode: query server via WebSocket const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); defer allocator.free(api_key_hash); const ws_url = try cfg.getWebSocketUrl(allocator); defer allocator.free(ws_url); var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); defer client.close(); try client.sendListExperiments(api_key_hash); // Receive response const response = try client.receiveMessage(allocator); defer allocator.free(response); // For now, just display raw response if (json) { std.debug.print("{s}\n", .{response}); } else { std.debug.print("{s}\n", .{response}); } } } fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void { if (args.len == 0) { core.output.err("experiment_id required"); return error.MissingArgument; } const exp_id = args[0]; const cfg = try config.Config.load(allocator); defer { var mut_cfg = cfg; mut_cfg.deinit(allocator); } const mode_result = try mode.detect(allocator, cfg); if (mode.isOffline(mode_result.mode)) { // Local mode: show from SQLite const db_path = try cfg.getDBPath(allocator); defer allocator.free(db_path); var database = try db.DB.init(allocator, db_path); defer database.close(); // Get experiment details const exp_sql = "SELECT experiment_id, name, description, created_at, status, synced FROM ml_experiments WHERE experiment_id = ?;"; const exp_stmt = try database.prepare(exp_sql); defer db.DB.finalize(exp_stmt); try db.DB.bindText(exp_stmt, 1, exp_id); if (!try db.DB.step(exp_stmt)) { core.output.err("Experiment not found"); return error.NotFound; } const name = db.DB.columnText(exp_stmt, 1); const description = db.DB.columnText(exp_stmt, 2); const created_at = db.DB.columnText(exp_stmt, 3); const status = db.DB.columnText(exp_stmt, 4); const synced = db.DB.columnInt64(exp_stmt, 5) != 0; // Get run count and last run date const runs_sql = "SELECT COUNT(*), MAX(start_time) FROM ml_runs WHERE experiment_id = ?;"; const runs_stmt = try database.prepare(runs_sql); defer db.DB.finalize(runs_stmt); try db.DB.bindText(runs_stmt, 1, exp_id); var run_count: i64 = 0; var last_run: ?[]const u8 = null; if (try db.DB.step(runs_stmt)) { run_count = db.DB.columnInt64(runs_stmt, 0); if (db.DB.columnText(runs_stmt, 1).len > 0) { last_run = try allocator.dupe(u8, db.DB.columnText(runs_stmt, 1)); } } defer if (last_run) |lr| allocator.free(lr); if (json) { std.debug.print("{{\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"description\":\"{s}\",\"status\":\"{s}\",\"created_at\":\"{s}\",\"synced\":{s},\"run_count\":{d},\"last_run\":\"{s}\"}}\n", .{ exp_id, name, description, status, created_at, if (synced) "true" else "false", run_count, last_run orelse "null", }); } else { std.debug.print("{s}\t{s}\t{s}\n", .{ name, exp_id, status }); if (description.len > 0) { std.debug.print("Description\t{s}\n", .{description}); } 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("LastRun\t{s}\n", .{lr}); } } } else { // Server mode: query server via WebSocket const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); defer allocator.free(api_key_hash); const ws_url = try cfg.getWebSocketUrl(allocator); defer allocator.free(ws_url); var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); defer client.close(); try client.sendGetExperimentByID(api_key_hash, exp_id); // Receive response const response = try client.receiveMessage(allocator); defer allocator.free(response); if (json) { std.debug.print("{s}\n", .{response}); } else { std.debug.print("{s}\n", .{response}); } } } fn generateExperimentID(allocator: std.mem.Allocator) ![]const u8 { return try uuid.generateV4(allocator); } fn printUsage() !void { std.debug.print("Usage: ml experiment [options]\n\n", .{}); std.debug.print("Subcommands:\n", .{}); std.debug.print("\tcreate --name [--description ]\tCreate new experiment\n", .{}); std.debug.print("\tlist\t\t\t\t\t\tList experiments\n", .{}); std.debug.print("\tshow \t\t\t\tShow experiment details\n", .{}); std.debug.print("\nOptions:\n", .{}); std.debug.print("\t--name \t\tExperiment name (required for create)\n", .{}); std.debug.print("\t--description \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("\tml experiment create --name \"baseline-cnn\"\n", .{}); std.debug.print("\tml experiment list\n", .{}); }