1231 lines · 48.4 KB · Zig
RawDownload
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.