fetch_ml/cli/src/commands/init.zig
Jeremie Fraeys a36a5e4522
feat(cli): add execution_mode config setting for local/remote/auto preference
Add execution_mode enum (local/remote/auto) to config for persistent
control over command execution behavior. Removes --local/--remote flags
from commands to simplify user workflow - no need to check server
connection status manually.

Changes:
- config.zig: Add ExecutionMode enum, execution_mode field, parsing/serialization
- mode.zig: Update detect() to check execution_mode == .local
- init.zig: Add --mode flag (local/remote/auto) for setting during init
- info.zig: Use config execution_mode, removed --local/--remote flags
- run.zig: Use config execution_mode, removed --local/--remote flags
- exec/mod.zig: Use config execution_mode, removed --local/--remote flags

Priority order for determining execution mode:
1. Config setting (execution_mode: local/remote/auto)
2. Auto-detect only if config is 'auto'

Users set mode once during init:
  ml init --mode=local     # Always use local
  ml init --mode=remote    # Always use remote
  ml init --mode=auto      # Auto-detect (default)
2026-03-05 12:18:30 -05:00

127 lines
5 KiB
Zig

const std = @import("std");
const Config = @import("../config.zig").Config;
const db = @import("../db.zig");
const core = @import("../core.zig");
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
var flags = core.flags.CommonFlags{};
var remaining = try core.flags.parseCommon(allocator, args, &flags);
defer remaining.deinit(allocator);
core.output.setMode(if (flags.json) .json else .text);
// Handle help flag early
if (flags.help) {
return printUsage();
}
// Parse CLI-specific overrides and flags
const cli_tracking_uri = core.flags.parseKVFlag(remaining.items, "tracking-uri");
const cli_artifact_path = core.flags.parseKVFlag(remaining.items, "artifact-path");
const cli_sync_uri = core.flags.parseKVFlag(remaining.items, "sync-uri");
const force_local = core.flags.parseBoolFlag(remaining.items, "local");
const cli_mode = core.flags.parseKVFlag(remaining.items, "mode");
var cfg = try Config.loadWithOverrides(allocator, cli_tracking_uri, cli_artifact_path, cli_sync_uri);
defer cfg.deinit(allocator);
// Apply mode preference from --mode flag
if (cli_mode) |mode| {
if (std.mem.eql(u8, mode, "local")) {
cfg.execution_mode = .local;
} else if (std.mem.eql(u8, mode, "remote")) {
cfg.execution_mode = .remote;
} else {
cfg.execution_mode = .auto;
}
}
// Print resolved config
std.debug.print("Resolved config:\n", .{});
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("\t(default)\n", .{});
} else {
std.debug.print("\n", .{});
}
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("\t(default)\n", .{});
} else {
std.debug.print("\n", .{});
}
std.debug.print("\tsync_uri = {s}\n", .{if (cfg.sync_uri.len > 0) cfg.sync_uri else "(not set)"});
std.debug.print("\texecution_mode = {s}\n", .{switch (cfg.execution_mode) {
.local => "local",
.remote => "remote",
.auto => "auto",
}});
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("\tLocal tracking DB will be created automatically if server becomes unavailable.\n", .{});
if (cfg.sync_uri.len > 0) {
std.debug.print("\tServer: {s}:{d}\n", .{ cfg.worker_host, cfg.worker_port });
}
return;
}
// --local path: create config + DB now
std.debug.print("(local mode explicitly requested)\n\n", .{});
// Get DB path from tracking URI
const db_path = try cfg.getDBPath(allocator);
defer allocator.free(db_path);
// Check if DB already exists
const db_exists = blk: {
std.fs.accessAbsolute(db_path, .{}) catch |err| {
if (err == error.FileNotFound) break :blk false;
};
break :blk true;
};
if (db_exists) {
std.debug.print("Database already exists: {s}\n", .{db_path});
} else {
// Create parent directories if needed
if (std.fs.path.dirname(db_path)) |dir| {
std.fs.makeDirAbsolute(dir) catch |err| {
if (err != error.PathAlreadyExists) {
std.log.err("Failed to create directory {s}: {}", .{ dir, err });
return error.MkdirFailed;
}
};
}
// Initialize database (creates schema)
var database = try db.DB.init(allocator, db_path);
defer database.close();
defer database.checkpointOnExit();
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("\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("\t--local\t\t\tCreate local database now (default: config only)\n", .{});
std.debug.print("\t--mode MODE\t\tDefault execution mode: local, remote, or auto (default: auto)\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", .{});
}