From 8040a9c0b1c7cb5dd025693df9710db409dc551f Mon Sep 17 00:00:00 2001 From: Thomas Edvalson Date: Sat, 22 Aug 2026 21:24:44 -0500 Subject: [PATCH] feat: support persisted local mode config --- README.md | 2 +- docs/references/steel-browser.md | 6 ++-- docs/references/steel-cli.md | 22 ++++++++------ src/commands/mod.rs | 13 ++++++++ src/util/api.rs | 51 +++++++++++++++++++++++++++----- 5 files changed, 74 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ac203a0..8fae37e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Supported shells: `bash`, `zsh`, `fish`, `powershell`, `elvish`. | API tools | `scrape`, `screenshot`, `pdf` | | Local runtime | `dev install`, `dev start`, `dev stop` | | Credentials | `credentials list`, `credentials create`, `credentials update`, `credentials delete` | -| Account and utility | `login`, `logout`, `config`, `doctor`, `cache`, `update`, `completion` | +| Account and utility | `login`, `logout`, `config`, `settings`, `doctor`, `cache`, `update`, `completion` | Full flags and schemas: [CLI reference](docs/cli-reference.md). diff --git a/docs/references/steel-browser.md b/docs/references/steel-browser.md index 56c869e..405afb1 100644 --- a/docs/references/steel-browser.md +++ b/docs/references/steel-browser.md @@ -26,10 +26,10 @@ Upstream command catalog (pinned reference): ## Modes -- Cloud mode (default): no mode flag required. -- Self-hosted mode: use `--local` or `--api-url `. +- Cloud mode (default): no mode flag or persisted local instance is required. +- Self-hosted mode: use `--local`, `--api-url `, or set `"instance": "local"` in `~/.config/steel/config.json`. -`--api-url` implies self-hosted mode. +`--api-url` implies self-hosted mode. A persisted local instance uses the configured self-hosted endpoint or falls back to `http://localhost:3000/v1`. ## Endpoint Resolution diff --git a/docs/references/steel-cli.md b/docs/references/steel-cli.md index d45673f..ced50c4 100644 --- a/docs/references/steel-cli.md +++ b/docs/references/steel-cli.md @@ -77,17 +77,21 @@ For generated flags and argument schemas, use [../cli-reference.md](../cli-refer - Browser session state: `~/.config/steel/browser-session-state.json` - Profile metadata: `~/.config/steel/profiles/.json` -On the first run where telemetry is enabled, the CLI prints a one-time notice to stderr describing what is collected and how to opt out. The notice is suppressed in JSON/non-TTY output. +## Main Config Values + +`~/.config/steel/config.json` supports these persisted values: -Telemetry can be disabled persistently in `config.json` with: +| Key | Purpose | +| --- | --- | +| `apiKey` | Cloud API key saved by `steel login`. | +| `name` | Saved account or CLI name associated with login. | +| `instance` | Default API mode: `"cloud"` or `"local"`. `steel settings` updates this value. | +| `browser.apiUrl` | Self-hosted Steel API endpoint used in Local mode. | +| `telemetry.disabled` | Set to `true` to disable telemetry. | -```json -{ - "telemetry": { - "disabled": true - } -} -``` +Local mode falls back to `http://localhost:3000/v1` when `browser.apiUrl` and the local endpoint environment variables are unset. Explicit `--local` and `--api-url` flags continue to select self-hosted mode for an invocation. + +On the first run where telemetry is enabled, the CLI prints a one-time notice to stderr describing what is collected and how to opt out. The notice is suppressed in JSON/non-TTY output. ## Environment Variables (Common) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 37d0325..60047a0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -15,6 +15,7 @@ pub mod profile; pub mod scrape; pub mod screenshot; pub mod sessions; +pub mod settings; pub mod skills; pub mod update; @@ -195,6 +196,7 @@ Other: steel login Login to Steel (alias: auth) steel logout Logout from Steel steel config Show current configuration + steel settings Change persisted CLI settings steel doctor Check environment, auth, and connectivity --preflight Only check auth and API (fast, for agents) steel forge [template] [-n ] Scaffold a new project from a template @@ -318,6 +320,9 @@ pub enum Command { /// Show current configuration Config(config::Args), + /// Manage persisted CLI settings + Settings(settings::Args), + /// Update to the latest version Update(update::Args), @@ -360,6 +365,7 @@ fn telemetry_command_path(command: &Command) -> Option { Command::Dev { command } => Some(format!("dev.{}", command.telemetry_name())), Command::Forge(_) => Some("forge".to_string()), Command::Config(_) => Some("config".to_string()), + Command::Settings(_) => Some("settings".to_string()), Command::Update(_) => Some("update".to_string()), Command::Cache(_) => Some("cache".to_string()), Command::Profile { command } => Some(format!("profile.{}", command.telemetry_name())), @@ -405,6 +411,7 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> { Command::Dev { command } => dev::run(command).await, Command::Forge(args) => forge::run(args).await, Command::Config(args) => config::run(args).await, + Command::Settings(args) => settings::run(args).await, Command::Update(args) => update::run(args).await, Command::Cache(args) => cache::run(args).await, Command::Profile { command } => profile::run(command).await, @@ -453,6 +460,12 @@ mod tests { out } + #[test] + fn settings_command_is_registered() { + let cli = Cli::try_parse_from(["steel", "settings"]).unwrap(); + assert!(matches!(cli.command, Command::Settings(_))); + } + #[tokio::test] async fn successful_command_emits_started_and_completed_events() { let _guard = env_lock().lock().await; diff --git a/src/util/api.rs b/src/util/api.rs index cb6cd53..3c33d71 100644 --- a/src/util/api.rs +++ b/src/util/api.rs @@ -18,24 +18,33 @@ pub fn init(local: bool, api_url: Option) { API_URL.get_or_init(|| api_url); } -fn is_local() -> bool { - LOCAL.load(Ordering::Relaxed) -} - fn api_url() -> Option<&'static str> { API_URL.get().and_then(|o| o.as_deref()) } -/// Resolve the API mode from global flags. +fn resolve_mode(local: bool, api_url: Option<&str>, configured_instance: Option<&str>) -> ApiMode { + ApiMode::resolve(local || configured_instance == Some("local"), api_url) +} + +/// Resolve the API mode from global flags + config. pub fn mode() -> ApiMode { - ApiMode::resolve(is_local(), api_url()) + let config = crate::config::settings::read_config().ok(); + resolve_mode( + LOCAL.load(Ordering::Relaxed), + api_url(), + config.as_ref().and_then(|c| c.instance.as_deref()), + ) } /// Resolve API mode and base URL from global flags + env + config. pub fn resolve() -> (ApiMode, String) { - let mode = ApiMode::resolve(is_local(), api_url()); let env_vars = EnvVars::from_env(); let config = crate::config::settings::read_config().ok(); + let mode = resolve_mode( + LOCAL.load(Ordering::Relaxed), + api_url(), + config.as_ref().and_then(|c| c.instance.as_deref()), + ); let local_config_url = config.as_ref().and_then(|c| c.local_api_url()); let base_url = mode.resolve_base_url(api_url(), &env_vars, local_config_url); (mode, base_url) @@ -47,3 +56,31 @@ pub fn resolve_with_auth() -> (ApiMode, String, Auth) { let auth = auth::resolve_auth(); (mode, base_url, auth) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn configured_local_instance_selects_local_mode() { + assert_eq!(resolve_mode(false, None, Some("local")), ApiMode::Local); + } + + #[test] + fn configured_cloud_instance_keeps_cloud_mode() { + assert_eq!(resolve_mode(false, None, Some("cloud")), ApiMode::Cloud); + } + + #[test] + fn explicit_api_url_still_selects_local_mode() { + assert_eq!( + resolve_mode(false, Some("http://steel.example/v1"), Some("cloud")), + ApiMode::Local + ); + } + + #[test] + fn local_flag_still_selects_local_mode() { + assert_eq!(resolve_mode(true, None, Some("cloud")), ApiMode::Local); + } +}