const std = @import("std");const builtin = @import("builtin");const app_process_runtime = @import("app_process_runtime.zig");const app_session_runtime = @import("app_session_runtime.zig");const auto_upgrade = @import("../upgrade/auto_upgrade.zig");const acp_runner = @import("../cli/acp_runner.zig");const cli_surface = @import("../cli/cli_surface.zig");const devbox_executor = @import("../execution/devbox_executor.zig");const background_process_provider = @import( "../execution/background_process_provider.zig",);const gateway_provider = @import("../gateway/gateway_provider.zig");const host = @import("../hosts/host.zig");const host_target = @import("../hosts/target.zig");const io_mod = @import("../shared/io.zig");const permission_auto_classifier = @import("../permissions/auto_classifier.zig");const prompt_policy = @import("../config/prompt_policy.zig");const sandbox = @import("../permissions/sandbox.zig");const skill_contract = @import("../skills/skill_contract.zig");const command_specs = @import("../slash_commands/command_specs.zig");const context_contract = @import("../workspace/context_contract.zig");const mode_registry = @import("../modes/mode_registry.zig");const mcp_contract = @import("../mcp/mcp_contract.zig");const mcp_runtime = @import("../mcp/mcp_runtime.zig");const tool_set_contract = @import("../tooling/tool_set.zig");const update_target = @import("../upgrade/update_target.zig");const test_builtin_gateway = if (builtin.is_test) @import("../../builtins/gateway.zig")else struct {};const test_builtin_commands = if (builtin.is_test) @import("../../builtins/commands.zig")else struct {}; const Allocator = std.mem.Allocator; pub const Config = struct { version: []const u8 = "", revision: []const u8 = "", build_channel: update_target.Channel = .stable, command_catalog: command_specs.TopLevelRegistry, default_model: []const u8, default_agent_step_limit: usize, models_path: []const u8, gateway_retry_count: usize, gateway_chat_url: []const u8, gateway_provider: gateway_provider.Provider, background_process_provider: background_process_provider.Provider = background_process_provider.unavailable_provider, url_opener: host.UrlOpener, secret_store: host.SecretStore, prompt_policy: prompt_policy.Policy, skill_root_policy: skill_contract.RootPolicy, ignored_list_entries: []const []const u8, max_list_entries: usize, max_read_file_bytes: usize, max_read_file_lines: usize, max_read_file_line_len: usize, max_command_output_bytes: usize, max_tool_result_bytes: usize, max_history_turns: usize, context_registry: context_contract.Registry, mode_registry: mode_registry.Registry, tool_set: tool_set_contract.ToolSet, inspect_mcp_profile_config: mcp_contract.InspectProfileConfigFn, load_mcp_runtime: mcp_runtime.LoadRuntimeFn, acp_runner: acp_runner.Runner, devbox_provider: ?devbox_executor.Provider = null, permission_reviewer_provider: ?permission_auto_classifier.Provider = null,}; pub fn run(comptime App: type, alloc: Allocator, args: []const [:0]const u8, cfg: Config) !void { const outcome = try runWithDeps(App, alloc, args, cfg, .{}); switch (outcome) { .returned => return, .exit => |code| std.process.exit(code), }} pub const RunOutcome = union(enum) { returned, exit: u8,}; pub const BeforeInteractiveResult = union(enum) { interactive: cli_surface.InteractiveLaunch, returned, exit: u8,}; const RunIfRequestedFn = *const fn (?*anyopaque, Allocator, []const [:0]const u8, cli_surface.Config) anyerror!cli_surface.RunResult;const GetenvFn = *const fn (?*anyopaque, []const u8) ?[]const u8;const WriteFn = *const fn (?*anyopaque, []const u8) anyerror!void;const FormatUnexpectedErrorFn = *const fn ([]u8, anyerror) anyerror![]const u8;const StartWorkerThreadFn = *const fn (?*anyopaque, *anyopaque) anyerror!void;const ReplaceProcessFn = *const fn ( ?*anyopaque, std.Io, std.process.ReplaceOptions,) std.process.ReplaceError;const RunDeps = struct { cli_ctx: ?*anyopaque = null, run_if_requested: RunIfRequestedFn = if (host_target.is_wasm) unavailableCliDispatch else runIfRequestedDefault, env_ctx: ?*anyopaque = null, getenv: GetenvFn = getenvDefault, stderr_ctx: ?*anyopaque = null, write_stderr: WriteFn = writeRealStderr, stdout_ctx: ?*anyopaque = null, write_stdout: WriteFn = writeRealStdout, format_unexpected_error: FormatUnexpectedErrorFn = formatUnexpectedError, worker_ctx: ?*anyopaque = null, start_worker_thread: ?StartWorkerThreadFn = null, replace_ctx: ?*anyopaque = null, replace_process: ReplaceProcessFn = replaceProcessDefault,}; fn runWithDeps(comptime App: type, alloc: Allocator, args: []const [:0]const u8, cfg: Config, deps: RunDeps) !RunOutcome { var launch: cli_surface.InteractiveLaunch = .{}; defer launch.deinit(alloc); const before = try runBeforeInteractiveWithDeps(alloc, args, cfg, deps); switch (before) { .interactive => |value| launch = value, .returned => return .returned, .exit => |code| return .{ .exit = code }, } return runInteractiveWithDeps(App, false, alloc, &launch, deps);} pub fn runBeforeInteractive(alloc: Allocator, args: []const [:0]const u8, cfg: Config) !BeforeInteractiveResult { _ = cli_surface.recordRequested(args) catch { writeStderr(.{}, cli_surface.record_modifier_usage); return .{ .exit = 1 }; }; const run_result = cli_surface.runIfRequested(alloc, args, cliSurfaceConfig(cfg)) catch |err| switch (err) { error.UnknownCliCommand => return .{ .exit = 1 }, else => { tryWriteErrorMessage(.{}, err); return .{ .exit = 1 }; }, }; return beforeInteractiveResultFromRunResult(alloc, run_result, benchEnabled());} pub fn runNoConfigBeforeInteractive( alloc: Allocator, args: []const [:0]const u8, version: []const u8, command_catalog: command_specs.TopLevelRegistry,) !?RunOutcome { return if (try cli_surface.runNoConfigIfRequested(alloc, args, version, command_catalog)) .returned else null;} fn runBeforeInteractiveWithDeps(alloc: Allocator, args: []const [:0]const u8, cfg: Config, deps: RunDeps) !BeforeInteractiveResult { _ = cli_surface.recordRequested(args) catch { writeStderr(deps, cli_surface.record_modifier_usage); return .{ .exit = 1 }; }; const run_result = deps.run_if_requested(deps.cli_ctx, alloc, args, cliSurfaceConfig(cfg)) catch |err| switch (err) { error.UnknownCliCommand => return .{ .exit = 1 }, else => { tryWriteErrorMessage(deps, err); return .{ .exit = 1 }; }, }; return beforeInteractiveResultFromRunResult(alloc, run_result, deps.getenv(deps.env_ctx, "FX_BENCH") != null);} fn beforeInteractiveResultFromRunResult(alloc: Allocator, run_result: cli_surface.RunResult, bench: bool) BeforeInteractiveResult { switch (run_result) { .interactive => |launch| { if (bench) { var mutable_launch = launch; mutable_launch.deinit(alloc); return .returned; } return .{ .interactive = launch }; }, .handled_success => return .returned, .handled_failure => return .{ .exit = 1 }, .handled_exit => |code| return if (code == 0) .returned else .{ .exit = code }, }} fn benchEnabled() bool { if (comptime builtin.link_libc) { return std.c.getenv("FX_BENCH") != null; } return io_mod.getenv("FX_BENCH") != null;} pub fn runInteractive(comptime App: type, alloc: Allocator, launch: *cli_surface.InteractiveLaunch) !RunOutcome { return runInteractiveWithDeps(App, false, alloc, launch, .{});} /// Runs the interactive product without native CLI dispatch, process replacement,/// or a worker thread. Single-threaded hosts must arrange cooperative prompt work.pub fn runInteractiveCooperative(comptime App: type, alloc: Allocator, launch: *cli_surface.InteractiveLaunch) !RunOutcome { return runInteractiveWithDeps(App, true, alloc, launch, .{});} fn unavailableCliDispatch(_: ?*anyopaque, _: Allocator, _: []const [:0]const u8, _: cli_surface.Config) anyerror!cli_surface.RunResult { return error.UnknownCliCommand;} fn runInteractiveWithDeps(comptime App: type, comptime cooperative: bool, alloc: Allocator, launch: *cli_surface.InteractiveLaunch, deps: RunDeps) !RunOutcome { const resume_requested = launch.requested_resume != null; var app = App.init(alloc, launch) catch |err| { switch (err) { error.NotATerminal => { writeStderr(deps, "fx requires an interactive terminal (TTY).\n"); return .{ .exit = 1 }; }, error.TerminalTooSmall => { writeStderr(deps, "fx needs at least 5 terminal rows.\n"); return .returned; }, error.RecordingStartFailed => { writeStderr(deps, "fx: unable to start terminal recording.\n"); return .{ .exit = 1 }; }, error.NoSavedSessions => { writeStderr(deps, "fx: no saved sessions for this workspace.\n"); return .{ .exit = 1 }; }, error.SessionNotFound => { writeStderr(deps, "fx: saved session not found.\n"); return .{ .exit = 1 }; }, error.SessionBusy => { writeStderr(deps, "fx: another Fx process may be using this session (running or suspended); check other terminals or run jobs, then use fg or quit that process\n"); return .{ .exit = 1 }; }, error.SessionLockUnsupported => { writeStderr(deps, "fx: the filesystem cannot provide the required session lock\n"); return .{ .exit = 1 }; }, error.SessionAuthorityBoundaryUnavailable, error.SessionCommitBoundaryUnavailable, => { writeStderr(deps, "fx: this session is being updated; wait a moment and retry\n"); return .{ .exit = 1 }; }, error.OneOffSessionNotResumable => { writeStderr(deps, "fx: one-off child sessions cannot accept additional prompts; create a persistent child to continue the conversation\n"); return .{ .exit = 1 }; }, error.InvalidSessionFormat => { writeStderr(deps, "fx: saved session is unreadable. Run `fx doctor`; if it is recoverable, use `fx session recover <id>`.\n"); return .{ .exit = 1 }; }, error.UnsupportedSessionSchema => { writeStderr(deps, "fx: saved session uses an unsupported version and cannot be resumed by this fx build.\n"); return .{ .exit = 1 }; }, error.RetiredSandboxValue, error.UnsupportedSandboxValue => { tryWriteErrorMessage(deps, err); return .{ .exit = 1 }; }, else => { reportUnexpectedInteractiveError(deps, err); return err; }, } }; if (comptime !cooperative) { if (@hasDecl(App, "startMcpDiscovery")) app.startMcpDiscovery(); if (@hasDecl(App, "rebindAfterInit")) app.rebindAfterInit(); } var app_needs_deinit = true; defer if (app_needs_deinit) app.deinit(); if (comptime !cooperative and @hasField(App, "session") and @hasDecl(@TypeOf(app.session), "attachProfileUsagePublisher")) { app.session.attachProfileUsagePublisher(app.alloc); } if (comptime !cooperative) { if (resume_requested) app.startResumedSessionReconciliation(); if (@hasDecl(App, "configureNotifications")) try app.configureNotifications(); if (@hasDecl(App, "playStartupSound")) app.playStartupSound(); if (@hasDecl(App, "startAutoUpgrade")) app.startAutoUpgrade(); if (@hasDecl(App, "startFileIndex")) app.startFileIndex(); startWorkerThread(App, &app, deps) catch |err| { app.releaseTerminal(); reportUnexpectedInteractiveError(deps, err); return err; }; app.startModelCacheWarmup(); } app.run() catch |err| { app.releaseTerminal(); if (err == error.TerminalInputClosed) return .returned; reportUnexpectedInteractiveError(deps, err); return err; }; const relaunch_request: ?auto_upgrade.RelaunchRequest = if (comptime cooperative) null else if (comptime @hasDecl(App, "takeUpgradeRelaunchRequest")) app.takeUpgradeRelaunchRequest() else null; const resume_handoff_columns: u16 = if (comptime cooperative) 0 else if (comptime @hasDecl(App, "resumeHandoffColumns")) app.resumeHandoffColumns() else 0; app_needs_deinit = false; const handoff_value = if (comptime cooperative) blk: { app.deinit(); break :blk null; } else app.deinitWithResumeHandoff(); if (relaunch_request) |request| { if (handoff_value) |value| { var handoff = value; defer handoff.deinit(alloc); var argv = [_][]const u8{ request.executablePath(), "resume", handoff.session_id, cli_surface.upgrade_relaunch_arg, }; const replace_err = deps.replace_process( deps.replace_ctx, io_mod.getIo(), .{ .argv = &argv }, ); writeUpgradeRelaunchFailure(deps, replace_err, handoff.session_id); } else { writeStderr( deps, "fx: upgrade installed, but no validated resume handoff was available. Your conversation remains on disk; run `fx doctor`.\n", ); } return .{ .exit = 1 }; } if (handoff_value) |value| { var handoff = value; defer handoff.deinit(alloc); var message_buffer: [512]u8 = undefined; const message = (if (comptime @hasDecl(App, "formatResumeHandoff")) App.formatResumeHandoff( &message_buffer, handoff.session_id, resume_handoff_columns, ) else formatResumeHandoff(&message_buffer, handoff.session_id)) catch return .returned; deps.write_stdout(deps.stdout_ctx, message) catch {}; } return .returned;} fn replaceProcessDefault( _: ?*anyopaque, zio: std.Io, options: std.process.ReplaceOptions,) std.process.ReplaceError { return std.process.replace(zio, options);} fn writeUpgradeRelaunchFailure( deps: RunDeps, err: std.process.ReplaceError, session_id: []const u8,) void { var buffer: [768]u8 = undefined; const message = std.fmt.bufPrint( &buffer, "fx: upgrade installed, but relaunch failed: {s}\nContinue session with: fx --resume {s}\n", .{ @errorName(err), session_id }, ) catch "fx: upgrade installed, but relaunch failed; run `fx doctor`.\n"; writeStderr(deps, message);} fn cliSurfaceConfig(cfg: Config) cli_surface.Config { return .{ .version = cfg.version, .revision = cfg.revision, .build_channel = cfg.build_channel, .command_catalog = cfg.command_catalog, .default_model = cfg.default_model, .default_agent_step_limit = cfg.default_agent_step_limit, .models_path = cfg.models_path, .gateway_retry_count = cfg.gateway_retry_count, .gateway_chat_url = cfg.gateway_chat_url, .gateway_provider = cfg.gateway_provider, .background_process_provider = cfg.background_process_provider, .url_opener = cfg.url_opener, .secret_store = cfg.secret_store, .prompt_policy = cfg.prompt_policy, .skill_root_policy = cfg.skill_root_policy, .ignored_list_entries = cfg.ignored_list_entries, .max_list_entries = cfg.max_list_entries, .max_read_file_bytes = cfg.max_read_file_bytes, .max_read_file_lines = cfg.max_read_file_lines, .max_read_file_line_len = cfg.max_read_file_line_len, .max_command_output_bytes = cfg.max_command_output_bytes, .max_tool_result_bytes = cfg.max_tool_result_bytes, .max_history_turns = cfg.max_history_turns, .context_registry = cfg.context_registry, .mode_registry = cfg.mode_registry, .tool_set = cfg.tool_set, .inspect_mcp_profile_config = cfg.inspect_mcp_profile_config, .load_mcp_runtime = cfg.load_mcp_runtime, .acp_runner = cfg.acp_runner, .devbox_provider = cfg.devbox_provider, .permission_reviewer_provider = cfg.permission_reviewer_provider, };} fn startWorkerThread(comptime App: type, app: *App, deps: RunDeps) !void { if (!@hasField(App, "worker_thread")) { const start = deps.start_worker_thread orelse return error.MissingWorkerThreadDependency; return start(deps.worker_ctx, @ptrCast(app)); } if (deps.start_worker_thread) |start| { return start(deps.worker_ctx, @ptrCast(app)); } try app_process_runtime.Runtime(App).startWorkerThread(app);} fn runIfRequestedDefault(_: ?*anyopaque, alloc: Allocator, args: []const [:0]const u8, cfg: cli_surface.Config) !cli_surface.RunResult { return cli_surface.runIfRequested(alloc, args, cfg);} fn getenvDefault(_: ?*anyopaque, key: []const u8) ?[]const u8 { return io_mod.getenv(key);} fn writeRealStderr(_: ?*anyopaque, text: []const u8) !void { try std.Io.File.stderr().writeStreamingAll(io_mod.getIo(), text);} fn writeRealStdout(_: ?*anyopaque, text: []const u8) !void { try std.Io.File.stdout().writeStreamingAll(io_mod.getIo(), text);} fn formatResumeHandoff(buffer: []u8, session_id: []const u8) ![]const u8 { return std.fmt.bufPrint( buffer, "Continue session with: fx --resume {s}\n", .{session_id}, );} fn formatUnexpectedError(buffer: []u8, err: anyerror) ![]const u8 { return std.fmt.bufPrint(buffer, "fx: {s}\n", .{@errorName(err)});} fn reportUnexpectedInteractiveError(deps: RunDeps, err: anyerror) void { var buffer: [256]u8 = undefined; const message = deps.format_unexpected_error(&buffer, err) catch return; deps.write_stderr(deps.stderr_ctx, message) catch {};} fn writeStderr(deps: RunDeps, text: []const u8) void { deps.write_stderr(deps.stderr_ctx, text) catch {};} fn tryWriteErrorMessage(deps: RunDeps, err: anyerror) void { writeStderr(deps, "fx: "); if (sandbox.configErrorMessage(err)) |message| { writeStderr(deps, message); } else { writeStderr(deps, @errorName(err)); } writeStderr(deps, "\n");} fn gatherNoopContextForTest(_: Allocator, _: context_contract.InitialContextInput) context_contract.ProviderError!context_contract.ProviderContext { return .{};} fn appendNoopStaticContextForTest(_: context_contract.StaticContextInput, _: Allocator, _: *std.ArrayList(@import("../shared/types.zig").ChatMessage)) context_contract.ProviderError!void {} fn appendNoopTransientContextForTest(_: context_contract.TransientContextInput, _: Allocator, _: *std.ArrayList(@import("../shared/types.zig").ChatMessage)) context_contract.ProviderError!void {} const test_entry_context_registry = context_contract.Registry{ .default_provider = .{ .id = "test.entry_context", .gather_project_context_fn = gatherNoopContextForTest, .select_applicable_project_context_fn = context_contract.selectNoApplicableProjectContext, .append_static_fn = appendNoopStaticContextForTest, .append_transient_fn = appendNoopTransientContextForTest,} }; fn unavailableDevboxForTest( _: ?*anyopaque, _: Allocator, _: []const u8, _: []const u8, _: devbox_executor.Control,Lines 1–500 of 1231. Load more
const std = @import("std"); const builtin = @import("builtin"); const app_process_runtime = @import("app_process_runtime.zig"); const app_session_runtime = @import("app_session_runtime.zig"); const auto_upgrade = @import("../upgrade/auto_upgrade.zig"); const acp_runner = @import("../cli/acp_runner.zig"); const cli_surface = @import("../cli/cli_surface.zig"); const devbox_executor = @import("../execution/devbox_executor.zig"); const background_process_provider = @import( "../execution/background_process_provider.zig", ); const gateway_provider = @import("../gateway/gateway_provider.zig"); const host = @import("../hosts/host.zig"); const host_target = @import("../hosts/target.zig"); const io_mod = @import("../shared/io.zig"); const permission_auto_classifier = @import("../permissions/auto_classifier.zig"); const prompt_policy = @import("../config/prompt_policy.zig"); const sandbox = @import("../permissions/sandbox.zig"); const skill_contract = @import("../skills/skill_contract.zig"); const command_specs = @import("../slash_commands/command_specs.zig"); const context_contract = @import("../workspace/context_contract.zig"); const mode_registry = @import("../modes/mode_registry.zig"); const mcp_contract = @import("../mcp/mcp_contract.zig"); const mcp_runtime = @import("../mcp/mcp_runtime.zig"); const tool_set_contract = @import("../tooling/tool_set.zig"); const update_target = @import("../upgrade/update_target.zig"); const test_builtin_gateway = if (builtin.is_test) @import("../../builtins/gateway.zig") else struct {}; const test_builtin_commands = if (builtin.is_test) @import("../../builtins/commands.zig") else struct {}; const Allocator = std.mem.Allocator; pub const Config = struct { version: []const u8 = "", revision: []const u8 = "", build_channel: update_target.Channel = .stable, command_catalog: command_specs.TopLevelRegistry, default_model: []const u8, default_agent_step_limit: usize, models_path: []const u8, gateway_retry_count: usize, gateway_chat_url: []const u8, gateway_provider: gateway_provider.Provider, background_process_provider: background_process_provider.Provider = background_process_provider.unavailable_provider, url_opener: host.UrlOpener, secret_store: host.SecretStore, prompt_policy: prompt_policy.Policy, skill_root_policy: skill_contract.RootPolicy, ignored_list_entries: []const []const u8, max_list_entries: usize, max_read_file_bytes: usize, max_read_file_lines: usize, max_read_file_line_len: usize, max_command_output_bytes: usize, max_tool_result_bytes: usize, max_history_turns: usize, context_registry: context_contract.Registry, mode_registry: mode_registry.Registry, tool_set: tool_set_contract.ToolSet, inspect_mcp_profile_config: mcp_contract.InspectProfileConfigFn, load_mcp_runtime: mcp_runtime.LoadRuntimeFn, acp_runner: acp_runner.Runner, devbox_provider: ?devbox_executor.Provider = null, permission_reviewer_provider: ?permission_auto_classifier.Provider = null, }; pub fn run(comptime App: type, alloc: Allocator, args: []const [:0]const u8, cfg: Config) !void { const outcome = try runWithDeps(App, alloc, args, cfg, .{}); switch (outcome) { .returned => return, .exit => |code| std.process.exit(code), } } pub const RunOutcome = union(enum) { returned, exit: u8, }; pub const BeforeInteractiveResult = union(enum) { interactive: cli_surface.InteractiveLaunch, returned, exit: u8, }; const RunIfRequestedFn = *const fn (?*anyopaque, Allocator, []const [:0]const u8, cli_surface.Config) anyerror!cli_surface.RunResult; const GetenvFn = *const fn (?*anyopaque, []const u8) ?[]const u8; const WriteFn = *const fn (?*anyopaque, []const u8) anyerror!void; const FormatUnexpectedErrorFn = *const fn ([]u8, anyerror) anyerror![]const u8; const StartWorkerThreadFn = *const fn (?*anyopaque, *anyopaque) anyerror!void; const ReplaceProcessFn = *const fn ( ?*anyopaque, std.Io, std.process.ReplaceOptions, ) std.process.ReplaceError; const RunDeps = struct { cli_ctx: ?*anyopaque = null, run_if_requested: RunIfRequestedFn = if (host_target.is_wasm) unavailableCliDispatch else runIfRequestedDefault, env_ctx: ?*anyopaque = null, getenv: GetenvFn = getenvDefault, stderr_ctx: ?*anyopaque = null, write_stderr: WriteFn = writeRealStderr, stdout_ctx: ?*anyopaque = null, write_stdout: WriteFn = writeRealStdout, format_unexpected_error: FormatUnexpectedErrorFn = formatUnexpectedError, worker_ctx: ?*anyopaque = null, start_worker_thread: ?StartWorkerThreadFn = null, replace_ctx: ?*anyopaque = null, replace_process: ReplaceProcessFn = replaceProcessDefault, }; fn runWithDeps(comptime App: type, alloc: Allocator, args: []const [:0]const u8, cfg: Config, deps: RunDeps) !RunOutcome { var launch: cli_surface.InteractiveLaunch = .{}; defer launch.deinit(alloc); const before = try runBeforeInteractiveWithDeps(alloc, args, cfg, deps); switch (before) { .interactive => |value| launch = value, .returned => return .returned, .exit => |code| return .{ .exit = code }, } return runInteractiveWithDeps(App, false, alloc, &launch, deps); } pub fn runBeforeInteractive(alloc: Allocator, args: []const [:0]const u8, cfg: Config) !BeforeInteractiveResult { _ = cli_surface.recordRequested(args) catch { writeStderr(.{}, cli_surface.record_modifier_usage); return .{ .exit = 1 }; }; const run_result = cli_surface.runIfRequested(alloc, args, cliSurfaceConfig(cfg)) catch |err| switch (err) { error.UnknownCliCommand => return .{ .exit = 1 }, else => { tryWriteErrorMessage(.{}, err); return .{ .exit = 1 }; }, }; return beforeInteractiveResultFromRunResult(alloc, run_result, benchEnabled()); } pub fn runNoConfigBeforeInteractive( alloc: Allocator, args: []const [:0]const u8, version: []const u8, command_catalog: command_specs.TopLevelRegistry, ) !?RunOutcome { return if (try cli_surface.runNoConfigIfRequested(alloc, args, version, command_catalog)) .returned else null; } fn runBeforeInteractiveWithDeps(alloc: Allocator, args: []const [:0]const u8, cfg: Config, deps: RunDeps) !BeforeInteractiveResult { _ = cli_surface.recordRequested(args) catch { writeStderr(deps, cli_surface.record_modifier_usage); return .{ .exit = 1 }; }; const run_result = deps.run_if_requested(deps.cli_ctx, alloc, args, cliSurfaceConfig(cfg)) catch |err| switch (err) { error.UnknownCliCommand => return .{ .exit = 1 }, else => { tryWriteErrorMessage(deps, err); return .{ .exit = 1 }; }, }; return beforeInteractiveResultFromRunResult(alloc, run_result, deps.getenv(deps.env_ctx, "FX_BENCH") != null); } fn beforeInteractiveResultFromRunResult(alloc: Allocator, run_result: cli_surface.RunResult, bench: bool) BeforeInteractiveResult { switch (run_result) { .interactive => |launch| { if (bench) { var mutable_launch = launch; mutable_launch.deinit(alloc); return .returned; } return .{ .interactive = launch }; }, .handled_success => return .returned, .handled_failure => return .{ .exit = 1 }, .handled_exit => |code| return if (code == 0) .returned else .{ .exit = code }, } } fn benchEnabled() bool { if (comptime builtin.link_libc) { return std.c.getenv("FX_BENCH") != null; } return io_mod.getenv("FX_BENCH") != null; } pub fn runInteractive(comptime App: type, alloc: Allocator, launch: *cli_surface.InteractiveLaunch) !RunOutcome { return runInteractiveWithDeps(App, false, alloc, launch, .{}); } /// Runs the interactive product without native CLI dispatch, process replacement, /// or a worker thread. Single-threaded hosts must arrange cooperative prompt work. pub fn runInteractiveCooperative(comptime App: type, alloc: Allocator, launch: *cli_surface.InteractiveLaunch) !RunOutcome { return runInteractiveWithDeps(App, true, alloc, launch, .{}); } fn unavailableCliDispatch(_: ?*anyopaque, _: Allocator, _: []const [:0]const u8, _: cli_surface.Config) anyerror!cli_surface.RunResult { return error.UnknownCliCommand; } fn runInteractiveWithDeps(comptime App: type, comptime cooperative: bool, alloc: Allocator, launch: *cli_surface.InteractiveLaunch, deps: RunDeps) !RunOutcome { const resume_requested = launch.requested_resume != null; var app = App.init(alloc, launch) catch |err| { switch (err) { error.NotATerminal => { writeStderr(deps, "fx requires an interactive terminal (TTY).\n"); return .{ .exit = 1 }; }, error.TerminalTooSmall => { writeStderr(deps, "fx needs at least 5 terminal rows.\n"); return .returned; }, error.RecordingStartFailed => { writeStderr(deps, "fx: unable to start terminal recording.\n"); return .{ .exit = 1 }; }, error.NoSavedSessions => { writeStderr(deps, "fx: no saved sessions for this workspace.\n"); return .{ .exit = 1 }; }, error.SessionNotFound => { writeStderr(deps, "fx: saved session not found.\n"); return .{ .exit = 1 }; }, error.SessionBusy => { writeStderr(deps, "fx: another Fx process may be using this session (running or suspended); check other terminals or run jobs, then use fg or quit that process\n"); return .{ .exit = 1 }; }, error.SessionLockUnsupported => { writeStderr(deps, "fx: the filesystem cannot provide the required session lock\n"); return .{ .exit = 1 }; }, error.SessionAuthorityBoundaryUnavailable, error.SessionCommitBoundaryUnavailable, => { writeStderr(deps, "fx: this session is being updated; wait a moment and retry\n"); return .{ .exit = 1 }; }, error.OneOffSessionNotResumable => { writeStderr(deps, "fx: one-off child sessions cannot accept additional prompts; create a persistent child to continue the conversation\n"); return .{ .exit = 1 }; }, error.InvalidSessionFormat => { writeStderr(deps, "fx: saved session is unreadable. Run `fx doctor`; if it is recoverable, use `fx session recover <id>`.\n"); return .{ .exit = 1 }; }, error.UnsupportedSessionSchema => { writeStderr(deps, "fx: saved session uses an unsupported version and cannot be resumed by this fx build.\n"); return .{ .exit = 1 }; }, error.RetiredSandboxValue, error.UnsupportedSandboxValue => { tryWriteErrorMessage(deps, err); return .{ .exit = 1 }; }, else => { reportUnexpectedInteractiveError(deps, err); return err; }, } }; if (comptime !cooperative) { if (@hasDecl(App, "startMcpDiscovery")) app.startMcpDiscovery(); if (@hasDecl(App, "rebindAfterInit")) app.rebindAfterInit(); } var app_needs_deinit = true; defer if (app_needs_deinit) app.deinit(); if (comptime !cooperative and @hasField(App, "session") and @hasDecl(@TypeOf(app.session), "attachProfileUsagePublisher")) { app.session.attachProfileUsagePublisher(app.alloc); } if (comptime !cooperative) { if (resume_requested) app.startResumedSessionReconciliation(); if (@hasDecl(App, "configureNotifications")) try app.configureNotifications(); if (@hasDecl(App, "playStartupSound")) app.playStartupSound(); if (@hasDecl(App, "startAutoUpgrade")) app.startAutoUpgrade(); if (@hasDecl(App, "startFileIndex")) app.startFileIndex(); startWorkerThread(App, &app, deps) catch |err| { app.releaseTerminal(); reportUnexpectedInteractiveError(deps, err); return err; }; app.startModelCacheWarmup(); } app.run() catch |err| { app.releaseTerminal(); if (err == error.TerminalInputClosed) return .returned; reportUnexpectedInteractiveError(deps, err); return err; }; const relaunch_request: ?auto_upgrade.RelaunchRequest = if (comptime cooperative) null else if (comptime @hasDecl(App, "takeUpgradeRelaunchRequest")) app.takeUpgradeRelaunchRequest() else null; const resume_handoff_columns: u16 = if (comptime cooperative) 0 else if (comptime @hasDecl(App, "resumeHandoffColumns")) app.resumeHandoffColumns() else 0; app_needs_deinit = false; const handoff_value = if (comptime cooperative) blk: { app.deinit(); break :blk null; } else app.deinitWithResumeHandoff(); if (relaunch_request) |request| { if (handoff_value) |value| { var handoff = value; defer handoff.deinit(alloc); var argv = [_][]const u8{ request.executablePath(), "resume", handoff.session_id, cli_surface.upgrade_relaunch_arg, }; const replace_err = deps.replace_process( deps.replace_ctx, io_mod.getIo(), .{ .argv = &argv }, ); writeUpgradeRelaunchFailure(deps, replace_err, handoff.session_id); } else { writeStderr( deps, "fx: upgrade installed, but no validated resume handoff was available. Your conversation remains on disk; run `fx doctor`.\n", ); } return .{ .exit = 1 }; } if (handoff_value) |value| { var handoff = value; defer handoff.deinit(alloc); var message_buffer: [512]u8 = undefined; const message = (if (comptime @hasDecl(App, "formatResumeHandoff")) App.formatResumeHandoff( &message_buffer, handoff.session_id, resume_handoff_columns, ) else formatResumeHandoff(&message_buffer, handoff.session_id)) catch return .returned; deps.write_stdout(deps.stdout_ctx, message) catch {}; } return .returned; } fn replaceProcessDefault( _: ?*anyopaque, zio: std.Io, options: std.process.ReplaceOptions, ) std.process.ReplaceError { return std.process.replace(zio, options); } fn writeUpgradeRelaunchFailure( deps: RunDeps, err: std.process.ReplaceError, session_id: []const u8, ) void { var buffer: [768]u8 = undefined; const message = std.fmt.bufPrint( &buffer, "fx: upgrade installed, but relaunch failed: {s}\nContinue session with: fx --resume {s}\n", .{ @errorName(err), session_id }, ) catch "fx: upgrade installed, but relaunch failed; run `fx doctor`.\n"; writeStderr(deps, message); } fn cliSurfaceConfig(cfg: Config) cli_surface.Config { return .{ .version = cfg.version, .revision = cfg.revision, .build_channel = cfg.build_channel, .command_catalog = cfg.command_catalog, .default_model = cfg.default_model, .default_agent_step_limit = cfg.default_agent_step_limit, .models_path = cfg.models_path, .gateway_retry_count = cfg.gateway_retry_count, .gateway_chat_url = cfg.gateway_chat_url, .gateway_provider = cfg.gateway_provider, .background_process_provider = cfg.background_process_provider, .url_opener = cfg.url_opener, .secret_store = cfg.secret_store, .prompt_policy = cfg.prompt_policy, .skill_root_policy = cfg.skill_root_policy, .ignored_list_entries = cfg.ignored_list_entries, .max_list_entries = cfg.max_list_entries, .max_read_file_bytes = cfg.max_read_file_bytes, .max_read_file_lines = cfg.max_read_file_lines, .max_read_file_line_len = cfg.max_read_file_line_len, .max_command_output_bytes = cfg.max_command_output_bytes, .max_tool_result_bytes = cfg.max_tool_result_bytes, .max_history_turns = cfg.max_history_turns, .context_registry = cfg.context_registry, .mode_registry = cfg.mode_registry, .tool_set = cfg.tool_set, .inspect_mcp_profile_config = cfg.inspect_mcp_profile_config, .load_mcp_runtime = cfg.load_mcp_runtime, .acp_runner = cfg.acp_runner, .devbox_provider = cfg.devbox_provider, .permission_reviewer_provider = cfg.permission_reviewer_provider, }; } fn startWorkerThread(comptime App: type, app: *App, deps: RunDeps) !void { if (!@hasField(App, "worker_thread")) { const start = deps.start_worker_thread orelse return error.MissingWorkerThreadDependency; return start(deps.worker_ctx, @ptrCast(app)); } if (deps.start_worker_thread) |start| { return start(deps.worker_ctx, @ptrCast(app)); } try app_process_runtime.Runtime(App).startWorkerThread(app); } fn runIfRequestedDefault(_: ?*anyopaque, alloc: Allocator, args: []const [:0]const u8, cfg: cli_surface.Config) !cli_surface.RunResult { return cli_surface.runIfRequested(alloc, args, cfg); } fn getenvDefault(_: ?*anyopaque, key: []const u8) ?[]const u8 { return io_mod.getenv(key); } fn writeRealStderr(_: ?*anyopaque, text: []const u8) !void { try std.Io.File.stderr().writeStreamingAll(io_mod.getIo(), text); } fn writeRealStdout(_: ?*anyopaque, text: []const u8) !void { try std.Io.File.stdout().writeStreamingAll(io_mod.getIo(), text); } fn formatResumeHandoff(buffer: []u8, session_id: []const u8) ![]const u8 { return std.fmt.bufPrint( buffer, "Continue session with: fx --resume {s}\n", .{session_id}, ); } fn formatUnexpectedError(buffer: []u8, err: anyerror) ![]const u8 { return std.fmt.bufPrint(buffer, "fx: {s}\n", .{@errorName(err)}); } fn reportUnexpectedInteractiveError(deps: RunDeps, err: anyerror) void { var buffer: [256]u8 = undefined; const message = deps.format_unexpected_error(&buffer, err) catch return; deps.write_stderr(deps.stderr_ctx, message) catch {}; } fn writeStderr(deps: RunDeps, text: []const u8) void { deps.write_stderr(deps.stderr_ctx, text) catch {}; } fn tryWriteErrorMessage(deps: RunDeps, err: anyerror) void { writeStderr(deps, "fx: "); if (sandbox.configErrorMessage(err)) |message| { writeStderr(deps, message); } else { writeStderr(deps, @errorName(err)); } writeStderr(deps, "\n"); } fn gatherNoopContextForTest(_: Allocator, _: context_contract.InitialContextInput) context_contract.ProviderError!context_contract.ProviderContext { return .{}; } fn appendNoopStaticContextForTest(_: context_contract.StaticContextInput, _: Allocator, _: *std.ArrayList(@import("../shared/types.zig").ChatMessage)) context_contract.ProviderError!void {} fn appendNoopTransientContextForTest(_: context_contract.TransientContextInput, _: Allocator, _: *std.ArrayList(@import("../shared/types.zig").ChatMessage)) context_contract.ProviderError!void {} const test_entry_context_registry = context_contract.Registry{ .default_provider = .{ .id = "test.entry_context", .gather_project_context_fn = gatherNoopContextForTest, .select_applicable_project_context_fn = context_contract.selectNoApplicableProjectContext, .append_static_fn = appendNoopStaticContextForTest, .append_transient_fn = appendNoopTransientContextForTest, } }; fn unavailableDevboxForTest( _: ?*anyopaque, _: Allocator, _: []const u8, _: []const u8, _: devbox_executor.Control,