fetch_ml/cli/src/config.zig
Jeremie Fraeys d225ea1f00 feat: implement Zig CLI with comprehensive ML experiment management
- Add modern CLI interface built with Zig for performance
- Include TUI (Terminal User Interface) with bubbletea-like features
- Implement ML experiment commands (run, status, manage)
- Add configuration management and validation
- Include shell completion scripts for bash and zsh
- Add comprehensive CLI testing framework
- Support for multiple ML frameworks and project types

CLI provides fast, efficient interface for ML experiment management
with modern terminal UI and comprehensive feature set.
2025-12-04 16:53:58 -05:00

155 lines
5.5 KiB
Zig

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);
}
};