- 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.
155 lines
5.5 KiB
Zig
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);
|
|
}
|
|
};
|