fetch_ml/cli/src/mode.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

108 lines
3.7 KiB
Zig

const std = @import("std");
const Config = @import("config.zig").Config;
const ws = @import("net/ws/client.zig");
/// Mode represents the operating mode of the CLI
pub const Mode = enum {
/// Local/offline mode - runs execute locally, tracking to SQLite
offline,
/// Online/runner mode - jobs queue to remote server
online,
};
/// DetectionResult includes the mode and any warning messages
pub const DetectionResult = struct {
mode: Mode,
warning: ?[]const u8,
};
/// Detect mode based on configuration and environment
/// Priority order (CLI — checked on every command):
/// 1. FETCHML_LOCAL=1 env var → local (forced, skip ping)
/// 2. force_local=true in config → local (forced, skip ping)
/// 3. cfg.Host == "" → local (not configured)
/// 4. API ping within 2s timeout → runner mode
/// - timeout / refused → local (fallback, log once per session)
/// - 401/403 → local (fallback, warn once about auth)
pub fn detect(allocator: std.mem.Allocator, cfg: Config) !DetectionResult {
// Priority 1: FETCHML_LOCAL env var
if (std.posix.getenv("FETCHML_LOCAL")) |val| {
if (std.mem.eql(u8, val, "1")) {
return .{ .mode = .offline, .warning = null };
}
}
// Priority 2: execution_mode in config
if (cfg.execution_mode == .local) {
return .{ .mode = .offline, .warning = null };
}
// Priority 3: No host configured
if (cfg.worker_host.len == 0) {
return .{ .mode = .offline, .warning = null };
}
// Priority 4: API ping with 2s timeout
const ping_result = try pingServer(allocator, cfg, 2000);
return switch (ping_result) {
.success => .{ .mode = .online, .warning = null },
.timeout => .{ .mode = .offline, .warning = "Server unreachable, falling back to local mode" },
.refused => .{ .mode = .offline, .warning = "Server connection refused, falling back to local mode" },
.auth_error => .{ .mode = .offline, .warning = "Authentication failed, falling back to local mode" },
};
}
/// PingResult represents the outcome of a server ping
const PingResult = enum {
success,
timeout,
refused,
auth_error,
};
/// Ping the server with a timeout - simplified version that just tries to connect
fn pingServer(allocator: std.mem.Allocator, cfg: Config, timeout_ms: u64) !PingResult {
_ = timeout_ms; // Timeout not implemented for this simplified version
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
var connection = ws.Client.connect(allocator, ws_url, cfg.api_key) catch |err| {
switch (err) {
error.ConnectionTimedOut => return .timeout,
error.ConnectionRefused => return .refused,
error.AuthenticationFailed => return .auth_error,
else => return .refused,
}
};
defer connection.close();
// Try to receive any message to confirm server is responding
const response = connection.receiveMessage(allocator) catch |err| {
switch (err) {
error.ConnectionTimedOut => return .timeout,
else => return .refused,
}
};
defer allocator.free(response);
return .success;
}
/// Check if mode is online
pub fn isOnline(mode: Mode) bool {
return mode == .online;
}
/// Check if mode is offline
pub fn isOffline(mode: Mode) bool {
return mode == .offline;
}
/// Require online mode, returning error if offline
pub fn requireOnline(mode: Mode, command_name: []const u8) !void {
if (mode == .offline) {
std.log.err("{s} requires server connection", .{command_name});
return error.RequiresServer;
}
}