From a36a5e45227bb2533b36506e507715709fe106c7 Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Thu, 5 Mar 2026 12:18:30 -0500 Subject: [PATCH] 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) --- cli/src/commands/exec/mod.zig | 20 +++++------------- cli/src/commands/info.zig | 17 +++------------- cli/src/commands/init.zig | 18 +++++++++++++++++ cli/src/commands/run.zig | 15 ++++---------- cli/src/config.zig | 38 +++++++++++++++++++++++------------ cli/src/mode.zig | 4 ++-- 6 files changed, 57 insertions(+), 55 deletions(-) diff --git a/cli/src/commands/exec/mod.zig b/cli/src/commands/exec/mod.zig index dfb7930..efa873a 100644 --- a/cli/src/commands/exec/mod.zig +++ b/cli/src/commands/exec/mod.zig @@ -29,8 +29,6 @@ pub const ExecOptions = struct { pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { var flags = core.flags.CommonFlags{}; var priority: u8 = 5; - var force_local = false; - var force_remote = false; // Find "--" separator var sep_index: ?usize = null; @@ -73,10 +71,6 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { i += 1; } else if (std.mem.eql(u8, arg, "--dry-run")) { options.dry_run = true; - } else if (std.mem.eql(u8, arg, "--local")) { - force_local = true; - } else if (std.mem.eql(u8, arg, "--remote")) { - force_remote = true; } else if (std.mem.eql(u8, arg, "--hypothesis") and i + 1 < pre.len) { options.hypothesis = pre[i + 1]; i += 1; @@ -128,18 +122,16 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { mut.deinit(allocator); } - // Determine execution mode + // Determine execution mode: config > auto-detect var exec_mode: ExecMode = undefined; - if (force_local) { + if (cfg.execution_mode == .local) { exec_mode = .local; - } else if (force_remote) { + } else if (cfg.execution_mode == .remote) { exec_mode = .remote; } else { - // Auto-detect const mode_result = try mode.detect(allocator, cfg); exec_mode = if (mode.isOnline(mode_result.mode)) .remote else .local; - if (mode_result.warning) |warn| { std.log.info("{s}", .{warn}); } @@ -182,10 +174,8 @@ fn printUsage() !void { \\ --gpu GPU devices requested (default: 0) \\ --gpu-memory GPU memory spec \\ - \\Execution mode: - \\ --local Force local execution - \\ --remote Force remote (fail if offline) - \\ (auto-detect if neither flag set) + \\Execution mode is controlled by execution_mode setting in config. + \\Use 'ml init --mode=local|remote|auto' to change. \\ \\Research context: \\ --hypothesis What you're testing diff --git a/cli/src/commands/info.zig b/cli/src/commands/info.zig index 2cd0ac0..5e20c46 100644 --- a/cli/src/commands/info.zig +++ b/cli/src/commands/info.zig @@ -10,16 +10,12 @@ const common = @import("common.zig"); pub const Options = struct { json: bool = false, base: ?[]const u8 = null, - local: bool = false, - remote: bool = false, }; pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { var flags = core.flags.CommonFlags{}; var base: ?[]const u8 = null; var target_path: ?[]const u8 = null; - var force_local = false; - var force_remote = false; var i: usize = 0; while (i < args.len) : (i += 1) { @@ -29,10 +25,6 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { } else if (std.mem.eql(u8, arg, "--base") and i + 1 < args.len) { base = args[i + 1]; i += 1; - } else if (std.mem.eql(u8, arg, "--local")) { - force_local = true; - } else if (std.mem.eql(u8, arg, "--remote")) { - force_remote = true; } else if (std.mem.startsWith(u8, arg, "--help")) { return printUsage(); } else if (std.mem.startsWith(u8, arg, "--")) { @@ -57,9 +49,8 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { mut_cfg.deinit(allocator); } - // Determine execution mode - const mode_result = try mode.detect(allocator, cfg); - const use_remote = if (force_local) false else if (force_remote) true else mode.isOnline(mode_result.mode); + // Determine execution mode: config > auto-detect + const use_remote = if (cfg.execution_mode == .local) false else if (cfg.execution_mode == .remote) true else mode.isOnline((try mode.detect(allocator, cfg)).mode); if (use_remote) { // Try remote query first @@ -226,12 +217,10 @@ fn displayRunInfo(allocator: std.mem.Allocator, root: std.json.ObjectMap, manife fn printUsage() !void { std.debug.print("Usage:\n", .{}); - std.debug.print("\tml info [--json] [--base ] [--local] [--remote]\n", .{}); + std.debug.print("\tml info [--json] [--base ]\n", .{}); std.debug.print("\nOptions:\n", .{}); std.debug.print("\t--json\t\tOutput machine-readable JSON\n", .{}); std.debug.print("\t--base \tBase path for resolving run manifests\n", .{}); - std.debug.print("\t--local\t\tForce local manifest lookup\n", .{}); - std.debug.print("\t--remote\tForce remote server query (fails if offline)\n", .{}); } test "resolveManifestPath uses run_manifest.json for directories" { diff --git a/cli/src/commands/init.zig b/cli/src/commands/init.zig index ccf4cf1..6c65b89 100644 --- a/cli/src/commands/init.zig +++ b/cli/src/commands/init.zig @@ -20,10 +20,22 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { 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}); @@ -42,6 +54,11 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { 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) @@ -102,6 +119,7 @@ fn printUsage() void { 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", .{}); diff --git a/cli/src/commands/run.zig b/cli/src/commands/run.zig index 087730d..940f377 100644 --- a/cli/src/commands/run.zig +++ b/cli/src/commands/run.zig @@ -32,8 +32,6 @@ pub const RunOptions = struct { /// Unified run command - transparently handles local and remote execution pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { var flags = core.flags.CommonFlags{}; - var force_local = false; - var force_remote = false; // Find "--" separator var sep_index: ?usize = null; @@ -80,10 +78,6 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { options.validate = true; } else if (std.mem.eql(u8, arg, "--explain")) { options.explain = true; - } else if (std.mem.eql(u8, arg, "--local")) { - force_local = true; - } else if (std.mem.eql(u8, arg, "--remote")) { - force_remote = true; } else if (std.mem.eql(u8, arg, "--force")) { options.force = true; } else if (std.mem.eql(u8, arg, "--hypothesis") and i + 1 < pre.len) { @@ -135,12 +129,12 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { mut.deinit(allocator); } - // Determine execution mode + // Determine execution mode: config > auto-detect var run_mode: RunMode = undefined; - if (force_local) { + if (cfg.execution_mode == .local) { run_mode = .local; - } else if (force_remote) { + } else if (cfg.execution_mode == .remote) { run_mode = .remote; } else { const mode_result = try mode.detect(allocator, cfg); @@ -203,13 +197,12 @@ fn explainJob(allocator: std.mem.Allocator, job_name: []const u8, options: *cons fn printUsage() !void { std.debug.print("Usage: ml run [options] [-- ]\n", .{}); std.debug.print("\nUnified run command - handles both local and remote execution.\n", .{}); + std.debug.print("Execution mode is controlled by execution_mode setting in config.\n", .{}); std.debug.print("\nOptions:\n", .{}); std.debug.print(" --priority <1-10> Job priority (default: 5)\n", .{}); std.debug.print(" --cpu CPU cores requested (default: 1)\n", .{}); std.debug.print(" --memory Memory GB requested (default: 4)\n", .{}); std.debug.print(" --gpu GPU devices requested (default: 0)\n", .{}); - std.debug.print(" --local Force local execution\n", .{}); - std.debug.print(" --remote Force remote execution\n", .{}); std.debug.print(" --dry-run Show what would happen\n", .{}); std.debug.print(" --validate Validate job without running\n", .{}); std.debug.print(" --explain Explain what will happen\n", .{}); diff --git a/cli/src/config.zig b/cli/src/config.zig index d6091bd..fc2b9aa 100644 --- a/cli/src/config.zig +++ b/cli/src/config.zig @@ -1,6 +1,12 @@ const std = @import("std"); const security = @import("security.zig"); +pub const ExecutionMode = enum { + local, + remote, + auto, +}; + pub const ExperimentConfig = struct { name: []const u8, entrypoint: []const u8, @@ -15,8 +21,6 @@ pub const Config = struct { artifact_path: []const u8, // Sync target URI (for pushing local runs to server) sync_uri: []const u8, - // Force local mode regardless of server config - force_local: bool, // Experiment configuration ([experiment] section) experiment: ?ExperimentConfig, @@ -33,6 +37,9 @@ pub const Config = struct { default_gpu: u8, default_gpu_memory: ?[]const u8, + // Execution mode preference (local, remote, auto) + execution_mode: ExecutionMode, + // CLI behavior defaults default_dry_run: bool, default_validate: bool, @@ -135,7 +142,6 @@ pub const Config = struct { .tracking_uri = try allocator.dupe(u8, "sqlite://./fetch_ml.db"), .artifact_path = try allocator.dupe(u8, "./experiments/"), .sync_uri = try allocator.dupe(u8, ""), - .force_local = false, .experiment = null, .worker_host = try allocator.dupe(u8, ""), .worker_user = try allocator.dupe(u8, ""), @@ -146,6 +152,7 @@ pub const Config = struct { .default_memory = 8, .default_gpu = 0, .default_gpu_memory = null, + .execution_mode = .auto, .default_dry_run = false, .default_validate = false, .default_json = false, @@ -162,7 +169,6 @@ pub const Config = struct { .tracking_uri = "", .artifact_path = "", .sync_uri = "", - .force_local = false, .experiment = null, .worker_host = "", .worker_user = "", @@ -173,6 +179,7 @@ pub const Config = struct { .default_memory = 8, .default_gpu = 0, .default_gpu_memory = null, + .execution_mode = .auto, .default_dry_run = false, .default_validate = false, .default_json = false, @@ -219,8 +226,6 @@ pub const Config = struct { config.artifact_path = try allocator.dupe(u8, value); } else if (std.mem.eql(u8, key, "sync_uri")) { config.sync_uri = try allocator.dupe(u8, value); - } else if (std.mem.eql(u8, key, "force_local")) { - config.force_local = std.mem.eql(u8, value, "true"); } else 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")) { @@ -247,8 +252,14 @@ pub const Config = struct { config.default_validate = std.mem.eql(u8, value, "true"); } else if (std.mem.eql(u8, key, "default_json")) { config.default_json = std.mem.eql(u8, value, "true"); - } else if (std.mem.eql(u8, key, "default_priority")) { - config.default_priority = try std.fmt.parseInt(u8, value, 10); + } else if (std.mem.eql(u8, key, "execution_mode")) { + if (std.mem.eql(u8, value, "local")) { + config.execution_mode = .local; + } else if (std.mem.eql(u8, value, "remote")) { + config.execution_mode = .remote; + } else { + config.execution_mode = .auto; + } } } } @@ -291,7 +302,6 @@ pub const Config = struct { \\tracking_uri = "{s}" \\artifact_path = "{s}" \\sync_uri = "{s}" - \\force_local = {s} \\{s} \\# Server config (for runner mode) \\worker_host = "{s}" @@ -306,6 +316,7 @@ pub const Config = struct { \\default_gpu = {d} \\{s} \\# CLI behavior defaults + \\execution_mode = "{s}" \\default_dry_run = {s} \\default_validate = {s} \\default_json = {s} @@ -315,7 +326,6 @@ pub const Config = struct { self.tracking_uri, self.artifact_path, self.sync_uri, - if (self.force_local) "true" else "false", if (self.experiment) |exp| try std.fmt.allocPrint(allocator, \\n[experiment]\nname = "{s}"\nentrypoint = "{s}"\n , .{ exp.name, exp.entrypoint }) else "", @@ -330,6 +340,11 @@ pub const Config = struct { if (self.default_gpu_memory) |gpu_mem| try std.fmt.allocPrint(allocator, \\default_gpu_memory = "{s}"\n , .{gpu_mem}) else "", + switch (self.execution_mode) { + .local => "local", + .remote => "remote", + .auto => "auto", + }, if (self.default_dry_run) "true" else "false", if (self.default_validate) "true" else "false", if (self.default_json) "true" else "false", @@ -367,9 +382,6 @@ pub const Config = struct { if (other.sync_uri.len > 0) { self.sync_uri = other.sync_uri; } - if (other.force_local) { - self.force_local = other.force_local; - } if (other.experiment) |exp| { if (self.experiment == null) { self.experiment = exp; diff --git a/cli/src/mode.zig b/cli/src/mode.zig index 7c47e52..f17d822 100644 --- a/cli/src/mode.zig +++ b/cli/src/mode.zig @@ -32,8 +32,8 @@ pub fn detect(allocator: std.mem.Allocator, cfg: Config) !DetectionResult { } } - // Priority 2: force_local in config - if (cfg.force_local) { + // Priority 2: execution_mode in config + if (cfg.execution_mode == .local) { return .{ .mode = .offline, .warning = null }; }