From e70f733197536288363b5f321e5658d5eea871f0 Mon Sep 17 00:00:00 2001 From: Aurora <261931150+TheAuroraAI@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:52:54 +0000 Subject: [PATCH] feat(config): expose FORGE_* env vars via :config-env command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2638 ## Summary Some Forge configurations were only discoverable by reading source code or documentation; they could only be changed by manually editing `.env` files. This adds a first-class `:config-env` (`:ce`) command and the underlying CLI plumbing so users can browse, inspect, and set all FORGE_* variables without ever touching a file directly. ## Changes ### CLI (`forge config get env` / `forge config set env`) * **`forge config get env [KEY]`** — prints the current value of a specific `FORGE_*` variable, or lists the entire catalogue (38 vars) with their default values and one-line descriptions when `KEY` is omitted. * **`forge config set env KEY VALUE`** — writes `KEY=VALUE` to the nearest `.env` file in the current working directory (creates the file if it does not exist; updates in-place if the key is already present) and applies the change to the running process immediately so a restart is not required. ### ZSH plugin (`:config-env` / `:ce`) * Opens an fzf picker populated by `forge config get env`, showing every `FORGE_*` variable with its current value. * Selecting a variable prompts for a new value and calls `forge config set env` automatically. * Registered in `built_in_commands.json` so it appears in `:help` and completion. --- crates/forge_main/src/built_in_commands.json | 4 + crates/forge_main/src/cli.rs | 13 ++ crates/forge_main/src/ui.rs | 134 +++++++++++++++++++ shell-plugin/lib/actions/config.zsh | 46 +++++++ shell-plugin/lib/dispatcher.zsh | 3 + 5 files changed, 200 insertions(+) diff --git a/crates/forge_main/src/built_in_commands.json b/crates/forge_main/src/built_in_commands.json index 8584b2cd85..edbccbc339 100644 --- a/crates/forge_main/src/built_in_commands.json +++ b/crates/forge_main/src/built_in_commands.json @@ -31,6 +31,10 @@ "command": "config-suggest-model", "description": "Set the model used for command suggestion generation [alias: csm]" }, + { + "command": "config-env", + "description": "Browse and set FORGE_* environment variables in .env [alias: ce]" + }, { "command": "config", "description": "List current configuration values" diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index c3f93a1db0..85ad5a30e5 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -567,6 +567,13 @@ pub enum ConfigSetField { /// Model ID to use for command suggestion generation. model: ModelId, }, + /// Set a FORGE_* environment variable in the local .env file. + Env { + /// The environment variable name (e.g., FORGE_TOOL_TIMEOUT). + key: String, + /// The value to assign. + value: String, + }, } /// Type-safe subcommands for `forge config get`. @@ -580,6 +587,12 @@ pub enum ConfigGetField { Commit, /// Get the command suggestion generation config. Suggest, + /// Get a FORGE_* environment variable's current value. + /// Omit KEY to list all known FORGE_* variables with their defaults. + Env { + /// The environment variable name (e.g., FORGE_TOOL_TIMEOUT). + key: Option, + }, } /// Command group for conversation management. diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index ad3231cdf1..c5c9c3cd2c 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -3428,6 +3428,9 @@ impl A + Send + Sync> UI { format!("is now the suggest model for provider '{provider}'"), ))?; } + ConfigSetField::Env { key, value } => { + self.handle_config_env_set(&key, &value)?; + } } Ok(()) @@ -3489,8 +3492,139 @@ impl A + Send + Sync> UI { None => self.writeln("Suggest: Not set")?, } } + ConfigGetField::Env { key } => { + self.handle_config_env_get(key.as_deref())?; + } + } + + Ok(()) + } + + /// Write a FORGE_* environment variable to the nearest `.env` file. + /// + /// If the key already exists it is updated in-place; otherwise it is + /// appended. The `.env` file is created in the current working directory + /// when it does not already exist. + fn handle_config_env_set(&mut self, key: &str, value: &str) -> Result<()> { + let cwd = self.api.environment().cwd; + let env_path = cwd.join(".env"); + + let existing = if env_path.exists() { + std::fs::read_to_string(&env_path)? + } else { + String::new() + }; + + let mut lines: Vec = existing.lines().map(|l| l.to_string()).collect(); + let prefix = format!("{key}="); + let new_line = format!("{key}={value}"); + let mut found = false; + for line in lines.iter_mut() { + if line.starts_with(&prefix) { + *line = new_line.clone(); + found = true; + break; + } + } + if !found { + lines.push(new_line); } + let mut content = lines.join("\n"); + if !content.ends_with('\n') { + content.push('\n'); + } + std::fs::write(&env_path, &content)?; + + // Apply to the running process immediately so the change takes effect + // without restarting forge. + unsafe { std::env::set_var(key, value) }; + + self.writeln_title( + TitleFormat::action(key).sub_title(format!("set to '{value}' in {}", env_path.display())), + )?; + Ok(()) + } + + /// Print the current value of one or all FORGE_* environment variables. + /// + /// When `key` is `None` every known FORGE_* variable is listed together + /// with its default value so users can discover what is configurable. + fn handle_config_env_get(&mut self, key: Option<&str>) -> Result<()> { + /// Catalogue of every user-facing FORGE_* environment variable. + struct EnvVar { + name: &'static str, + default: &'static str, + description: &'static str, + } + + const KNOWN_VARS: &[EnvVar] = &[ + EnvVar { name: "FORGE_TOOL_TIMEOUT", default: "300", description: "Max seconds a shell tool is allowed to run" }, + EnvVar { name: "FORGE_MAX_SEARCH_RESULT_BYTES", default: "10240", description: "Max bytes returned by a file-search result" }, + EnvVar { name: "FORGE_STDOUT_MAX_LINE_LENGTH", default: "2000", description: "Truncate stdout lines longer than this" }, + EnvVar { name: "FORGE_MAX_LINE_LENGTH", default: "2000", description: "Max line length for file reads" }, + EnvVar { name: "FORGE_MAX_FILE_READ_BATCH_SIZE",default: "auto", description: "Max files read in parallel (default: 2×CPU)" }, + EnvVar { name: "FORGE_PARALLEL_FILE_READS", default: "auto", description: "File-read parallelism (default: 2×CPU)" }, + EnvVar { name: "FORGE_MAX_IMAGE_SIZE", default: "10485760", description: "Max image attachment size in bytes (10 MiB)" }, + EnvVar { name: "FORGE_MAX_CONVERSATIONS", default: "100", description: "Max conversations kept in history" }, + EnvVar { name: "FORGE_SEM_SEARCH_LIMIT", default: "200", description: "Max candidates for semantic search" }, + EnvVar { name: "FORGE_SEM_SEARCH_TOP_K", default: "20", description: "Top-K results returned by semantic search" }, + EnvVar { name: "FORGE_MAX_EXTENSIONS", default: "15", description: "Max skill extensions loaded" }, + EnvVar { name: "FORGE_MODEL_CACHE_TTL", default: "604800", description: "Model list cache TTL in seconds (1 week)" }, + EnvVar { name: "FORGE_HISTORY_FILE", default: "", description: "Custom path for shell history file" }, + EnvVar { name: "FORGE_API_URL", default: "https://antinomy.ai/api/v1/", description: "Forge backend API base URL" }, + EnvVar { name: "FORGE_WORKSPACE_SERVER_URL", default: "https://api.forgecode.dev/", description: "Workspace server base URL" }, + EnvVar { name: "FORGE_AUTO_DUMP", default: "", description: "Auto-dump conversation on exit: json | html" }, + EnvVar { name: "FORGE_DUMP_AUTO_OPEN", default: "false", description: "Open dump file automatically after saving" }, + EnvVar { name: "FORGE_DEBUG_REQUESTS", default: "", description: "Directory to write raw HTTP request logs" }, + EnvVar { name: "FORGE_RETRY_MAX_ATTEMPTS", default: "3", description: "Max retry attempts for failed HTTP requests" }, + EnvVar { name: "FORGE_RETRY_INITIAL_BACKOFF_MS",default: "1000", description: "Initial retry backoff in milliseconds" }, + EnvVar { name: "FORGE_RETRY_BACKOFF_FACTOR", default: "2", description: "Exponential backoff multiplier" }, + EnvVar { name: "FORGE_RETRY_STATUS_CODES", default: "429,500,502,503,504", description: "Comma-separated HTTP status codes to retry" }, + EnvVar { name: "FORGE_SUPPRESS_RETRY_ERRORS", default: "false", description: "Suppress error output during retries" }, + EnvVar { name: "FORGE_HTTP_CONNECT_TIMEOUT", default: "30", description: "HTTP connection timeout in seconds" }, + EnvVar { name: "FORGE_HTTP_READ_TIMEOUT", default: "120", description: "HTTP read timeout in seconds" }, + EnvVar { name: "FORGE_HTTP_POOL_IDLE_TIMEOUT", default: "90", description: "HTTP connection pool idle timeout in seconds" }, + EnvVar { name: "FORGE_HTTP_POOL_MAX_IDLE_PER_HOST", default: "10", description: "Max idle HTTP connections per host" }, + EnvVar { name: "FORGE_HTTP_MAX_REDIRECTS", default: "10", description: "Max HTTP redirects followed" }, + EnvVar { name: "FORGE_HTTP_USE_HICKORY", default: "false", description: "Use Hickory DNS resolver instead of system" }, + EnvVar { name: "FORGE_HTTP_TLS_BACKEND", default: "native", description: "TLS backend: native | rustls" }, + EnvVar { name: "FORGE_HTTP_MIN_TLS_VERSION", default: "", description: "Minimum TLS version: 1.2 | 1.3" }, + EnvVar { name: "FORGE_HTTP_MAX_TLS_VERSION", default: "", description: "Maximum TLS version: 1.2 | 1.3" }, + EnvVar { name: "FORGE_HTTP_ADAPTIVE_WINDOW", default: "false", description: "Enable HTTP/2 adaptive flow-control window" }, + EnvVar { name: "FORGE_HTTP_KEEP_ALIVE_INTERVAL",default: "15", description: "TCP keep-alive probe interval in seconds (none to disable)" }, + EnvVar { name: "FORGE_HTTP_KEEP_ALIVE_TIMEOUT", default: "15", description: "TCP keep-alive timeout in seconds" }, + EnvVar { name: "FORGE_HTTP_KEEP_ALIVE_WHILE_IDLE", default: "true", description: "Send keep-alive probes while connection is idle" }, + EnvVar { name: "FORGE_HTTP_ACCEPT_INVALID_CERTS", default: "false", description: "Skip TLS certificate validation (insecure)" }, + EnvVar { name: "FORGE_HTTP_ROOT_CERT_PATHS", default: "", description: "Comma-separated paths to extra CA certificate files" }, + ]; + + if let Some(k) = key { + let upper_key = k.to_uppercase(); + match std::env::var(&upper_key) { + Ok(val) => self.writeln(format!("{upper_key}={val}"))?, + Err(_) => { + // Show default if key is known + if let Some(entry) = KNOWN_VARS.iter().find(|e| e.name == upper_key) { + self.writeln(format!("{upper_key} (not set, default: {})", entry.default))?; + } else { + self.writeln(format!("{upper_key} is not set"))?; + } + } + } + return Ok(()); + } + + // List all known variables + self.writeln_title(TitleFormat::action("FORGE_* environment variables"))?; + for ev in KNOWN_VARS { + let current = std::env::var(ev.name).ok(); + let value_display = match ¤t { + Some(v) => format!("{v} (set)"), + None => format!("{} (default)", ev.default), + }; + self.writeln(format!("{:<40} {} # {}", ev.name, value_display, ev.description))?; + } Ok(()) } diff --git a/shell-plugin/lib/actions/config.zsh b/shell-plugin/lib/actions/config.zsh index 324cac1b24..65b6dc408d 100644 --- a/shell-plugin/lib/actions/config.zsh +++ b/shell-plugin/lib/actions/config.zsh @@ -240,6 +240,52 @@ function _forge_action_suggest_model() { ) } +# Action handler: Browse and set FORGE_* environment variables. +# +# Displays an fzf list of all known FORGE_* variables with their current +# values. Selecting a variable prompts for a new value and writes it to +# the nearest .env file via `forge config set env KEY VALUE`. +function _forge_action_config_env() { + local input_text="$1" + echo + + # Fetch the full variable list from the CLI. + local env_list + env_list=$($_FORGE_BIN config get env 2>/dev/null) + if [[ -z "$env_list" ]]; then + _forge_log error "Could not retrieve FORGE_* variable list" + return 0 + fi + + local selected + selected=$(echo "$env_list" \ + | grep -v '^$' \ + | grep -v 'FORGE_\* environment' \ + | _forge_fzf \ + --prompt="Env Var ❯ " \ + --query="$input_text" \ + --delimiter=" " \ + --with-nth=1 \ + --preview="echo {} | sed 's/ /\n/g'" \ + --preview-window="bottom:3:wrap") + + if [[ -n "$selected" ]]; then + # Extract just the variable name (first whitespace-delimited token). + local var_name + var_name=$(echo "$selected" | awk '{print $1}') + + # Prompt for the new value using zle read-from-minibuffer if available, + # otherwise fall back to plain `read`. + local new_value + echo -n "Set ${var_name}= " + read -r new_value + + if [[ -n "$new_value" ]]; then + _forge_exec config set env "$var_name" "$new_value" + fi + fi +} + # Action handler: Sync workspace for codebase search function _forge_action_sync() { echo diff --git a/shell-plugin/lib/dispatcher.zsh b/shell-plugin/lib/dispatcher.zsh index 1f4b539b45..4287731603 100644 --- a/shell-plugin/lib/dispatcher.zsh +++ b/shell-plugin/lib/dispatcher.zsh @@ -184,6 +184,9 @@ function forge-accept-line() { config-suggest-model|csm) _forge_action_suggest_model "$input_text" ;; + config-env|ce) + _forge_action_config_env "$input_text" + ;; tools|t) _forge_action_tools ;;