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: force_local in config if (cfg.force_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; } }