const std = @import("std"); pub const Config = struct { worker_host: []const u8, worker_user: []const u8, worker_base: []const u8, worker_port: u16, api_key: []const u8, pub fn validate(self: Config) !void { // Validate host if (self.worker_host.len == 0) { return error.EmptyHost; } // Validate port range if (self.worker_port == 0 or self.worker_port > 65535) { return error.InvalidPort; } // Validate API key format (should be hex string) if (self.api_key.len == 0) { return error.EmptyAPIKey; } // Check if API key is valid hex for (self.api_key) |char| { if (!((char >= '0' and char <= '9') or (char >= 'a' and char <= 'f') or (char >= 'A' and char <= 'F'))) { return error.InvalidAPIKeyFormat; } } // Validate base path if (self.worker_base.len == 0) { return error.EmptyBasePath; } } pub fn load(allocator: std.mem.Allocator) !Config { const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; const config_path = try std.fmt.allocPrint(allocator, "{s}/.ml/config.toml", .{home}); defer allocator.free(config_path); const file = std.fs.openFileAbsolute(config_path, .{}) catch |err| { if (err == error.FileNotFound) { std.debug.print("Config file not found. Run 'ml init' first.\n", .{}); return error.ConfigNotFound; } return err; }; defer file.close(); // Load config with environment variable overrides var config = try loadFromFile(allocator, file); // Apply environment variable overrides if (std.posix.getenv("ML_HOST")) |host| { config.worker_host = try allocator.dupe(u8, host); } if (std.posix.getenv("ML_USER")) |user| { config.worker_user = try allocator.dupe(u8, user); } if (std.posix.getenv("ML_BASE")) |base| { config.worker_base = try allocator.dupe(u8, base); } if (std.posix.getenv("ML_PORT")) |port_str| { config.worker_port = try std.fmt.parseInt(u16, port_str, 10); } if (std.posix.getenv("ML_API_KEY")) |api_key| { config.api_key = try allocator.dupe(u8, api_key); } try config.validate(); return config; } fn loadFromFile(allocator: std.mem.Allocator, file: std.fs.File) !Config { const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); // Simple TOML parser - parse key=value pairs var config = Config{ .worker_host = "", .worker_user = "", .worker_base = "", .worker_port = 22, .api_key = "", }; var lines = std.mem.splitScalar(u8, content, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); if (trimmed.len == 0 or trimmed[0] == '#') continue; var parts = std.mem.splitScalar(u8, trimmed, '='); const key = std.mem.trim(u8, parts.next() orelse continue, " \t"); const value_raw = std.mem.trim(u8, parts.next() orelse continue, " \t"); // Remove quotes const value = if (value_raw.len >= 2 and value_raw[0] == '"' and value_raw[value_raw.len - 1] == '"') value_raw[1 .. value_raw.len - 1] else value_raw; if (std.mem.eql(u8, key, "worker_host")) { config.worker_host = try allocator.dupe(u8, value); } else if (std.mem.eql(u8, key, "worker_user")) { config.worker_user = try allocator.dupe(u8, value); } else if (std.mem.eql(u8, key, "worker_base")) { config.worker_base = try allocator.dupe(u8, value); } else if (std.mem.eql(u8, key, "worker_port")) { config.worker_port = try std.fmt.parseInt(u16, value, 10); } else if (std.mem.eql(u8, key, "api_key")) { config.api_key = try allocator.dupe(u8, value); } } return config; } pub fn save(self: Config, allocator: std.mem.Allocator) !void { const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; // Create .ml directory const ml_dir = try std.fmt.allocPrint(allocator, "{s}/.ml", .{home}); defer allocator.free(ml_dir); std.fs.makeDirAbsolute(ml_dir) catch |err| { if (err != error.PathAlreadyExists) return err; }; const config_path = try std.fmt.allocPrint(allocator, "{s}/config.toml", .{ml_dir}); defer allocator.free(config_path); const file = try std.fs.createFileAbsolute(config_path, .{}); defer file.close(); const writer = file.writer(); try writer.print("worker_host = \"{s}\"\n", .{self.worker_host}); try writer.print("worker_user = \"{s}\"\n", .{self.worker_user}); try writer.print("worker_base = \"{s}\"\n", .{self.worker_base}); try writer.print("worker_port = {d}\n", .{self.worker_port}); try writer.print("api_key = \"{s}\"\n", .{self.api_key}); } pub fn deinit(self: *Config, allocator: std.mem.Allocator) void { allocator.free(self.worker_host); allocator.free(self.worker_user); allocator.free(self.worker_base); allocator.free(self.api_key); } };