From 367fd9302bbcf77ec1912f861c2f20aea0335b7c Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 8 Apr 2026 16:27:25 -0400 Subject: [PATCH] refactor(cli): centralize command settings and server access Add CommandContext to load machine settings once per invocation, cache server access, and route migrated commands through the shared ServerStoreClient path instead of reloading settings and reconnecting ad hoc. --- lib/crates/fabro-cli/src/command_context.rs | 111 ++++++++++++++++++ .../fabro-cli/src/commands/artifact/mod.rs | 4 +- .../fabro-cli/src/commands/config/mod.rs | 28 +++-- lib/crates/fabro-cli/src/commands/doctor.rs | 40 ++++++- lib/crates/fabro-cli/src/commands/graph.rs | 8 +- lib/crates/fabro-cli/src/commands/model.rs | 45 ++----- .../fabro-cli/src/commands/pr/create.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/list.rs | 4 +- lib/crates/fabro-cli/src/commands/pr/mod.rs | 33 ++---- .../fabro-cli/src/commands/preflight.rs | 12 +- .../fabro-cli/src/commands/provider/login.rs | 8 +- .../fabro-cli/src/commands/repo/init.rs | 20 +++- .../fabro-cli/src/commands/run/command.rs | 14 +-- lib/crates/fabro-cli/src/commands/run/cp.rs | 4 +- .../fabro-cli/src/commands/run/create.rs | 16 +-- lib/crates/fabro-cli/src/commands/run/diff.rs | 4 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 4 +- lib/crates/fabro-cli/src/commands/run/logs.rs | 4 +- lib/crates/fabro-cli/src/commands/run/mod.rs | 16 ++- .../fabro-cli/src/commands/run/preview.rs | 4 +- .../fabro-cli/src/commands/run/resume.rs | 4 +- .../fabro-cli/src/commands/run/rewind.rs | 4 +- lib/crates/fabro-cli/src/commands/run/ssh.rs | 4 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 4 +- .../fabro-cli/src/commands/runs/inspect.rs | 4 +- .../fabro-cli/src/commands/runs/list.rs | 4 +- lib/crates/fabro-cli/src/commands/runs/rm.rs | 4 +- .../fabro-cli/src/commands/secret/list.rs | 3 +- .../fabro-cli/src/commands/secret/mod.rs | 40 ++----- .../fabro-cli/src/commands/secret/rm.rs | 3 +- .../fabro-cli/src/commands/secret/set.rs | 3 +- .../fabro-cli/src/commands/system/df.rs | 14 +-- .../fabro-cli/src/commands/system/events.rs | 10 +- .../fabro-cli/src/commands/system/info.rs | 11 +- .../fabro-cli/src/commands/system/prune.rs | 11 +- lib/crates/fabro-cli/src/commands/validate.rs | 8 +- lib/crates/fabro-cli/src/main.rs | 1 + lib/crates/fabro-cli/src/server_client.rs | 53 ++++----- lib/crates/fabro-cli/src/server_runs.rs | 9 +- 39 files changed, 345 insertions(+), 232 deletions(-) create mode 100644 lib/crates/fabro-cli/src/command_context.rs diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs new file mode 100644 index 000000000..8c33954d0 --- /dev/null +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -0,0 +1,111 @@ +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context as _, Result, bail}; +use fabro_types::Settings; +use tokio::sync::OnceCell; + +use crate::args::{ServerConnectionArgs, ServerTargetArgs}; +use crate::server_client::ServerStoreClient; +use crate::{server_client, user_config}; + +#[derive(Clone, Debug)] +pub(crate) enum ServerMode { + None, + ByTarget { + target_override: Option, + }, + ByStorageDir { + target_override: Option, + storage_dir_override: Option, + }, +} + +pub(crate) struct CommandContext { + cwd: PathBuf, + base_config_path: PathBuf, + machine_settings: Settings, + server_mode: ServerMode, + server: OnceCell>, +} + +impl CommandContext { + pub(crate) fn base() -> Result { + Self::new(ServerMode::None) + } + + pub(crate) fn for_target(args: &ServerTargetArgs) -> Result { + Self::new(ServerMode::ByTarget { + target_override: args.server.clone(), + }) + } + + pub(crate) fn for_connection(args: &ServerConnectionArgs) -> Result { + Self::new(ServerMode::ByStorageDir { + target_override: args.target.server.clone(), + storage_dir_override: args.storage_dir.clone_path(), + }) + } + + fn new(server_mode: ServerMode) -> Result { + let cwd = std::env::current_dir().context("Failed to get current directory")?; + let base_config_path = user_config::active_settings_path(None); + let machine_settings = match &server_mode { + ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?, + ServerMode::ByStorageDir { + storage_dir_override, + .. + } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, + }; + + Ok(Self { + cwd, + base_config_path, + machine_settings, + server_mode, + server: OnceCell::new(), + }) + } + + pub(crate) fn cwd(&self) -> &Path { + &self.cwd + } + + pub(crate) fn base_config_path(&self) -> &Path { + &self.base_config_path + } + + pub(crate) fn machine_settings(&self) -> &Settings { + &self.machine_settings + } + + pub(crate) async fn server(&self) -> Result> { + let server_mode = self.server_mode.clone(); + let base_config_path = self.base_config_path.clone(); + let machine_settings = self.machine_settings.clone(); + + let client = self + .server + .get_or_try_init(|| async move { + let target = match server_mode { + ServerMode::None => bail!("This command context does not have server access"), + ServerMode::ByTarget { target_override } + | ServerMode::ByStorageDir { + target_override, .. + } => ServerTargetArgs { + server: target_override, + }, + }; + server_client::connect_server_with_settings( + &target, + &machine_settings, + &base_config_path, + ) + .await + .map(Arc::new) + }) + .await?; + + Ok(Arc::clone(client)) + } +} diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 093fdfda3..0395903f8 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -5,6 +5,7 @@ use anyhow::{Context, Result}; use fabro_types::{RunId, StageId}; use crate::args::{ArtifactCommand, ArtifactNamespace, GlobalArgs, ServerTargetArgs}; +use crate::command_context::CommandContext; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerSummaryLookup; @@ -24,7 +25,8 @@ pub(super) async fn resolve_artifacts( node: Option<&str>, retry: Option, ) -> Result<(RunId, ServerStoreClient, Vec)> { - let lookup = ServerSummaryLookup::connect(server).await?; + let ctx = CommandContext::for_target(server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_selector)?; let run_id = run.run_id(); let mut entries = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 6d7c3c4c0..24698410b 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -2,7 +2,7 @@ use std::io::Write; use std::path::Path; use crate::args::{GlobalArgs, SettingsArgs}; -use crate::server_client; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config; use fabro_config::ConfigLayer; @@ -11,13 +11,19 @@ use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSetting use fabro_config::project; use fabro_types::Settings; -fn config_layers(workflow: Option<&Path>) -> anyhow::Result { - let cwd = std::env::current_dir()?; +fn config_layers( + ctx: &CommandContext, + workflow: Option<&Path>, +) -> anyhow::Result { + let cwd = ctx.cwd(); let (workflow_layer, project_layer) = match workflow { - Some(path) => workflow_and_project_layers(path, &cwd)?, - None => (ConfigLayer::default(), ConfigLayer::project(&cwd)?), + Some(path) => workflow_and_project_layers(path, cwd)?, + None => (ConfigLayer::default(), ConfigLayer::project(cwd)?), }; - let user_layer = user_config::settings_layer_with_storage_dir(None)?; + let user_layer = user_config::settings_layer_with_config_and_storage_dir( + Some(ctx.base_config_path()), + None, + )?; Ok(EffectiveSettingsLayers::new( ConfigLayer::default(), workflow_layer, @@ -52,7 +58,8 @@ fn workflow_and_project_layers( } async fn merged_config(args: &SettingsArgs) -> anyhow::Result { - let layers = config_layers(args.workflow.as_deref())?; + let base_ctx = CommandContext::base()?; + let layers = config_layers(&base_ctx, args.workflow.as_deref())?; if args.local { return effective_settings::resolve_settings( layers, @@ -61,10 +68,9 @@ async fn merged_config(args: &SettingsArgs) -> anyhow::Result { ); } - let machine_settings = user_config::load_settings()?; - let target = user_config::resolve_server_target(&args.target, &machine_settings)?; - let client = server_client::connect_server_only(&args.target).await?; - let server_settings = client.retrieve_server_settings().await?; + let ctx = CommandContext::for_target(&args.target)?; + let target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?; + let server_settings = ctx.server().await?.retrieve_server_settings().await?; let mode = match target { user_config::ServerTarget::HttpUrl { .. } => EffectiveSettingsMode::RemoteServer, user_config::ServerTarget::UnixSocket(_) => EffectiveSettingsMode::LocalDaemon, diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index f7a227f46..ac74284ba 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -18,7 +18,7 @@ use regex::Regex; use semver::Version; use crate::args::{DoctorArgs, GlobalArgs}; -use crate::server_client; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; pub(crate) struct DepSpec { @@ -357,8 +357,38 @@ pub(crate) async fn run_doctor( }], }; - let client = match server_client::connect_server_backed_api_client(&args.target).await { - Ok(client) => client, + let ctx = match CommandContext::for_target(&args.target) { + Ok(ctx) => ctx, + Err(err) => { + report.sections.push(CheckSection { + title: "Server".to_string(), + checks: vec![CheckResult { + name: "Fabro server".to_string(), + status: CheckStatus::Error, + summary: "settings resolution failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], + remediation: Some( + "Fix the local CLI settings or provide `--server`, then run doctor again." + .to_string(), + ), + }], + }); + + if let Some(spinner) = spinner { + spinner.finish_and_clear(); + } + + if globals.json { + print_json_pretty(&report)?; + } else { + render_report(&report, &styles, verbose); + } + return Ok(1); + } + }; + + let server = match ctx.server().await { + Ok(server) => server, Err(err) => { report.sections.push(CheckSection { title: "Server".to_string(), @@ -387,7 +417,7 @@ pub(crate) async fn run_doctor( } }; - let health = match client.get_health().send().await { + let health = match server.api().get_health().send().await { Ok(response) => response.into_inner(), Err(err) => { report.sections.push(CheckSection { @@ -420,7 +450,7 @@ pub(crate) async fn run_doctor( .checks .push(check_version_parity(&health.version)); - match client.run_diagnostics().send().await { + match server.api().run_diagnostics().send().await { Ok(response) => { let diagnostics = response.into_inner(); report diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 0ad4f70db..6487f5818 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -7,9 +7,9 @@ use fabro_util::terminal::Styles; use tracing::debug; use crate::args::{GlobalArgs, GraphArgs, GraphDirection, GraphOutputFormat}; +use crate::command_context::CommandContext; use crate::commands::run::output::api_diagnostics_to_local; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest}; -use crate::server_client; use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( @@ -21,15 +21,15 @@ pub(crate) async fn run( globals.require_no_json()?; } - let cwd = std::env::current_dir()?; + let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), - cwd, + cwd: ctx.cwd().to_path_buf(), args_layer: ConfigLayer::default(), args: None, run_id: None, })?; - let client = server_client::connect_server_only(&args.target).await?; + let client = ctx.server().await?; let preflight = client.run_preflight(built.manifest.clone()).await?; let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics); diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 08a347abc..6d7739279 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result, bail}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::{self, types as api_types}; @@ -8,6 +8,7 @@ use serde::Serialize; use serde::de::DeserializeOwned; use crate::args::{GlobalArgs, ModelListArgs, ModelTestArgs, ModelsCommand}; +use crate::command_context::CommandContext; use crate::server_client; #[derive(Serialize)] @@ -42,9 +43,10 @@ pub(crate) async fn execute(command: Option, globals: &GlobalArgs ModelsCommand::List(args) => &args.target, ModelsCommand::Test(args) => &args.target, }; - let client = server_client::connect_server_backed_api_client(target_args).await?; + let ctx = CommandContext::for_target(target_args)?; + let server = ctx.server().await?; - run_models(command, client, globals.json).await + run_models(command, server.api(), globals.json).await } fn format_context_window(tokens: i64) -> String { @@ -171,33 +173,6 @@ fn model_test_row_from_status(model: &Model, status: &str, result_color: Color) } } -fn map_api_error(err: progenitor_client::Error) -> anyhow::Error -where - E: serde::Serialize + std::fmt::Debug, -{ - match err { - progenitor_client::Error::ErrorResponse(response) => { - let status = response.status(); - if let Ok(value) = serde_json::to_value(response.into_inner()) { - if let Some(detail) = value - .get("errors") - .and_then(serde_json::Value::as_array) - .and_then(|errors| errors.first()) - .and_then(|entry| entry.get("detail")) - .and_then(serde_json::Value::as_str) - { - return anyhow!("{detail}"); - } - } - anyhow!("request failed with status {status}") - } - progenitor_client::Error::UnexpectedResponse(response) => { - anyhow!("request failed with status {}", response.status()) - } - other => anyhow!("{other}"), - } -} - fn convert_type(value: TInput) -> Result where TInput: serde::Serialize, @@ -223,7 +198,7 @@ async fn fetch_models_from_server( request = request.query(query.to_string()); } - let response = request.send().await.map_err(map_api_error)?; + let response = request.send().await.map_err(server_client::map_api_error)?; let parsed = response.into_inner(); let count = parsed.data.len() as u64; models.extend(convert_type::<_, Vec>(parsed.data)?); @@ -245,7 +220,7 @@ async fn test_model_via_server( if let Some(mode) = mode { request = request.mode(mode); } - let response = request.send().await.map_err(map_api_error)?; + let response = request.send().await.map_err(server_client::map_api_error)?; Ok(response.into_inner()) } @@ -393,7 +368,7 @@ async fn test_models_via_server( #[allow(clippy::print_stdout)] async fn run_models( command: ModelsCommand, - client: fabro_api::Client, + client: &fabro_api::Client, json_output: bool, ) -> Result<()> { let styles = Styles::detect_stdout(); @@ -403,7 +378,7 @@ async fn run_models( provider, query, .. }) => { let models = - fetch_models_from_server(&client, provider.as_deref(), query.as_deref()).await?; + fetch_models_from_server(client, provider.as_deref(), query.as_deref()).await?; if json_output { println!("{}", serde_json::to_string_pretty(&models)?); @@ -418,7 +393,7 @@ async fn run_models( .. }) => { test_models_via_server( - &client, + client, provider.as_deref(), model.as_deref(), deep, diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 5ae23ebe6..03cbb2db8 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -6,6 +6,7 @@ use fabro_workflow::pull_request::maybe_open_pull_request; use tracing::info; use crate::args::{GlobalArgs, PrCreateArgs}; +use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -16,7 +17,8 @@ pub(super) async fn create_command( github_app: Option, globals: &GlobalArgs, ) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); let events = lookup.client().list_run_events(&run_id, None, None).await?; diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 1494d4a54..2f7d15d0f 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -4,6 +4,7 @@ use serde::Serialize; use tracing::info; use crate::args::{GlobalArgs, PrListArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -24,7 +25,8 @@ pub(super) async fn list_command( let creds = github_app.context( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", )?; - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let mut entries = Vec::new(); for run in lookup.runs() { diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index 577b14bfc..1ac56df86 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -9,37 +9,21 @@ use anyhow::{Context, Result}; use fabro_types::PullRequestRecord; use crate::args::{GlobalArgs, PrCommand, PrNamespace, ServerTargetArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::github::build_github_app_credentials; -use crate::user_config::load_settings; pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<()> { + let ctx = CommandContext::base()?; + let github_app = build_github_app_credentials(ctx.machine_settings().app_id())?; match ns.command { PrCommand::Create(args) => { - let cli_settings = load_settings()?; - let github_app = build_github_app_credentials(cli_settings.app_id())?; Box::pin(create::create_command(args, github_app, globals)).await } - PrCommand::List(args) => { - let cli_settings = load_settings()?; - let github_app = build_github_app_credentials(cli_settings.app_id())?; - list::list_command(args, github_app, globals).await - } - PrCommand::View(args) => { - let cli_settings = load_settings()?; - let github_app = build_github_app_credentials(cli_settings.app_id())?; - view::view_command(args, github_app, globals).await - } - PrCommand::Merge(args) => { - let cli_settings = load_settings()?; - let github_app = build_github_app_credentials(cli_settings.app_id())?; - merge::merge_command(args, github_app, globals).await - } - PrCommand::Close(args) => { - let cli_settings = load_settings()?; - let github_app = build_github_app_credentials(cli_settings.app_id())?; - close::close_command(args, github_app, globals).await - } + PrCommand::List(args) => list::list_command(args, github_app, globals).await, + PrCommand::View(args) => view::view_command(args, github_app, globals).await, + PrCommand::Merge(args) => merge::merge_command(args, github_app, globals).await, + PrCommand::Close(args) => close::close_command(args, github_app, globals).await, } } @@ -47,7 +31,8 @@ pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { - let lookup = ServerSummaryLookup::connect(server).await?; + let ctx = CommandContext::for_target(server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_id)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 55ba46e1c..d15bffb6e 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -3,28 +3,26 @@ use fabro_config::ConfigLayer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, PreflightArgs}; +use crate::command_context::CommandContext; use crate::commands::run::output::{ api_check_report_to_local, api_diagnostics_to_local, print_preflight_workflow_summary, }; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, preflight_manifest_args}; -use crate::server_client; use crate::shared::print_json_pretty; -use crate::user_config; pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let cli_settings = user_config::load_settings()?; - args.verbose = args.verbose || cli_settings.verbose_enabled(); + let ctx = CommandContext::for_target(&args.target)?; + args.verbose = args.verbose || ctx.machine_settings().verbose_enabled(); - let cwd = std::env::current_dir()?; let manifest = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), - cwd, + cwd: ctx.cwd().to_path_buf(), args_layer: ConfigLayer::try_from(&args)?, args: preflight_manifest_args(&args), run_id: None, })?; - let client = server_client::connect_server_only(&args.target).await?; + let client = ctx.server().await?; let response = client.run_preflight(manifest.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index d070573fe..33a6537e0 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -6,13 +6,14 @@ use fabro_util::terminal::Styles; use tokio::task::spawn_blocking; use crate::args::{GlobalArgs, ProviderLoginArgs}; -use crate::server_client; +use crate::command_context::CommandContext; use crate::shared::provider_auth; pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) -> Result<()> { globals.require_no_json()?; let s = Styles::detect_stderr(); - let client = server_client::connect_server_backed_api_client(&args.target).await?; + let ctx = CommandContext::for_target(&args.target)?; + let server = ctx.server().await?; let use_oauth = args.provider == Provider::OpenAi && spawn_blocking(|| provider_auth::prompt_confirm("Log in via browser (OAuth)?", true)) @@ -36,7 +37,8 @@ pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) } for (name, value) in env_pairs { - client + server + .api() .set_secret() .name(name.clone()) .body(types::SetSecretRequest { value }) diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 1023668c9..9595762ab 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail}; use tokio::task::spawn_blocking; use crate::args::{GlobalArgs, RepoInitArgs, ServerTargetArgs}; -use crate::server_client; +use crate::command_context::CommandContext; pub(super) fn git_repo_root() -> Result { let output = std::process::Command::new("git") @@ -166,15 +166,24 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { return; // Not a GitHub repo — skip silently }; - let client = match server_client::connect_server_backed_api_client(target).await { - Ok(client) => client, + let ctx = match CommandContext::for_target(target) { + Ok(ctx) => ctx, + Err(err) => { + eprintln!("\n Warning: could not resolve fabro server settings: {err}"); + return; + } + }; + + let server = match ctx.server().await { + Ok(server) => server, Err(err) => { eprintln!("\n Warning: could not connect to fabro server: {err}"); return; } }; - let check = match client + let check = match server + .api() .get_github_repo() .owner(owner.clone()) .name(repo.clone()) @@ -214,7 +223,8 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { }) .await; - match client + match server + .api() .get_github_repo() .owner(owner.clone()) .name(repo.clone()) diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 4803fbf03..cc3c054fb 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -2,19 +2,19 @@ use anyhow::Result; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, RunArgs}; -use crate::server_client; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -use crate::user_config::{self, settings_layer_with_storage_dir}; +use crate::user_config::settings_layer_with_storage_dir; pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let cli_settings = user_config::load_settings()?; + let ctx = CommandContext::for_target(&args.target)?; let cli = settings_layer_with_storage_dir(None)?; - args.verbose = args.verbose || cli_settings.verbose_enabled(); + args.verbose = args.verbose || ctx.machine_settings().verbose_enabled(); let quiet = args.detach; - let prevent_idle_sleep = cli_settings.prevent_idle_sleep_enabled(); - let created_run = Box::pin(super::create::create_run(&args, cli, styles, quiet)).await?; + let prevent_idle_sleep = ctx.machine_settings().prevent_idle_sleep_enabled(); + let created_run = Box::pin(super::create::create_run(&ctx, &args, cli, styles, quiet)).await?; #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = crate::sleep_inhibitor::guard(prevent_idle_sleep); @@ -22,7 +22,7 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<( #[cfg(not(feature = "sleep_inhibitor"))] let _ = prevent_idle_sleep; - let client = server_client::connect_server_only(&args.target).await?; + let client = ctx.server().await?; super::start::start_run_with_client(&client, &created_run.run_id, false).await?; if args.detach { diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index a63d95b73..99a64f311 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -5,6 +5,7 @@ use tokio::fs; use tracing::{debug, info}; use crate::args::{CpArgs, GlobalArgs, ServerTargetArgs}; +use crate::command_context::CommandContext; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerSummaryLookup; use crate::shared::{print_json_pretty, split_run_path}; @@ -117,7 +118,8 @@ async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, ) -> Result<(ServerStoreClient, fabro_types::RunId)> { - let lookup = ServerSummaryLookup::connect(server).await?; + let ctx = CommandContext::for_target(server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_prefix)?; Ok((lookup.client().clone_for_reuse(), run.run_id())) } diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index ab37a982f..db6fae923 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -1,14 +1,14 @@ use std::path::PathBuf; use crate::args::RunArgs; +use crate::command_context::CommandContext; use fabro_config::ConfigLayer; use fabro_config::Storage; -use fabro_types::{RunId, Settings}; +use fabro_types::RunId; use fabro_util::terminal::Styles; use super::output::{api_diagnostics_to_local, print_preflight_workflow_summary}; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manifest_args}; -use crate::server_client; use crate::user_config::{self, ServerTarget}; pub(crate) struct CreatedRun { @@ -20,6 +20,7 @@ pub(crate) struct CreatedRun { /// /// This does NOT execute the workflow — it only prepares the run directory. pub(crate) async fn create_run( + ctx: &CommandContext, args: &RunArgs, cli_defaults: ConfigLayer, styles: &Styles, @@ -30,13 +31,12 @@ pub(crate) async fn create_run( .as_ref() .ok_or_else(|| anyhow::anyhow!("--workflow is required"))?; let cli_args_config = ConfigLayer::try_from(args)?; - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let _settings: Settings = cli_args_config + let cwd = ctx.cwd().to_path_buf(); + let _settings = cli_args_config .clone() .combine(ConfigLayer::for_workflow(workflow_path, &cwd)?) .combine(cli_defaults) .resolve()?; - let machine_settings = user_config::load_settings()?; let run_id = args .run_id .as_deref() @@ -51,8 +51,8 @@ pub(crate) async fn create_run( args: run_manifest_args(args), run_id, })?; - let target = user_config::resolve_server_target(&args.target, &machine_settings)?; - let client = server_client::connect_server_only(&args.target).await?; + let target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?; + let client = ctx.server().await?; if !quiet { let preflight = client.run_preflight(built.manifest.clone()).await?; let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics); @@ -67,7 +67,7 @@ pub(crate) async fn create_run( let created_run_id = client.create_run_from_manifest(built.manifest).await?; let local_run_dir = match &target { ServerTarget::UnixSocket(_) => Some( - Storage::new(machine_settings.storage_dir()) + Storage::new(ctx.machine_settings().storage_dir()) .run_scratch(&created_run_id) .root() .to_path_buf(), diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 3db803ffb..81ab88890 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -4,13 +4,15 @@ use anyhow::{Context, Result, bail}; use tracing::{debug, info}; use crate::args::{DiffArgs, GlobalArgs}; +use crate::command_context::CommandContext; use crate::server_client::RunProjection; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 4be007b5e..0975ea296 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -6,6 +6,7 @@ use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_r use git2::Repository; use crate::args::{ForkArgs, GlobalArgs}; +use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -13,7 +14,8 @@ use crate::shared::repo::ensure_matching_repo_origin; pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 267ceeb99..9cde08d5e 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -11,6 +11,7 @@ use tokio::time; use tracing::{debug, info}; use crate::args::{GlobalArgs, LogsArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::server_runs::ServerSummaryLookup; use crate::shared::format_usd_micros; @@ -18,7 +19,8 @@ use crate::shared::format_usd_micros; const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let client = lookup.client(); diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 8652ce4ef..30f0f2262 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -2,6 +2,7 @@ use anyhow::Result; use fabro_util::terminal::Styles; use crate::args::{AttachArgs, GlobalArgs, RunArgs, RunCommands, RunWorkerArgs, StartArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; use crate::user_config::settings_layer_with_storage_dir; @@ -40,7 +41,8 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( apply_json_defaults(&mut args, globals); let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); let cli = settings_layer_with_storage_dir(None)?; - let created_run = Box::pin(create::create_run(&args, cli, styles, true)).await?; + let ctx = CommandContext::for_target(&args.target)?; + let created_run = Box::pin(create::create_run(&ctx, &args, cli, styles, true)).await?; if globals.json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { @@ -49,7 +51,8 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( Ok(()) } RunCommands::Start(StartArgs { server, run }) => { - let lookup = ServerSummaryLookup::connect(&server).await?; + let ctx = CommandContext::for_target(&server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); start::start_run_with_client(lookup.client(), &run_id, false).await?; @@ -60,7 +63,8 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( } RunCommands::Attach(AttachArgs { server, run }) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let lookup = ServerSummaryLookup::connect(&server).await?; + let ctx = CommandContext::for_target(&server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); let exit_code = attach::attach_run_with_client( @@ -92,8 +96,8 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = { - let cli_settings = crate::user_config::load_settings()?; - crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled()) + let ctx = CommandContext::for_target(&args.server)?; + crate::sleep_inhibitor::guard(ctx.machine_settings().prevent_idle_sleep_enabled()) }; resume::resume_command(args, styles, globals).await } @@ -103,7 +107,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( } RunCommands::Fork(args) => { let styles = Styles::detect_stderr(); - fork::run(&args, &styles, globals).await + Box::pin(fork::run(&args, &styles, globals)).await } RunCommands::Wait(args) => { let styles = Styles::detect_stderr(); diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index d6460eb07..ecdd4b3ae 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -2,11 +2,13 @@ use anyhow::{Context, Result}; use tracing::info; use crate::args::{GlobalArgs, PreviewArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); let expires_in_secs = diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index da2f8b4e9..b79e52c21 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,6 +1,7 @@ use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, ResumeArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -14,7 +15,8 @@ pub(crate) async fn resume_command( styles: &'static Styles, globals: &GlobalArgs, ) -> anyhow::Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index e85242f59..f5d027abd 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -14,6 +14,7 @@ use git2::Repository; use serde::Serialize; use crate::args::{GlobalArgs, RewindArgs}; +use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerSummaryLookup; @@ -30,7 +31,8 @@ pub(crate) struct TimelineEntryJson { pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index f531cc7b8..725622c24 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -2,6 +2,7 @@ use anyhow::{Result, bail}; use tracing::info; use crate::args::{GlobalArgs, SshArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -10,7 +11,8 @@ pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> { globals.require_no_json()?; } - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); let ssh = lookup diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index a457f16e2..198c0a9fe 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -8,6 +8,7 @@ use fabro_workflow::run_status::RunStatus; use tracing::info; use crate::args::{GlobalArgs, WaitArgs}; +use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::{format_duration_ms, format_usd_micros}; @@ -17,7 +18,8 @@ const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_millis const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_secs(3); pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&args.run)?; let client = lookup.client(); diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 6558fad58..c3192e8c3 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -4,6 +4,7 @@ use serde::Serialize; use fabro_workflow::run_status::RunStatus; use crate::args::{GlobalArgs, InspectArgs}; +use crate::command_context::CommandContext; use crate::server_client::RunProjection; use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup}; @@ -19,7 +20,8 @@ pub(crate) struct InspectOutput { } pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index d75761a7a..e3e3de71e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -10,6 +10,7 @@ use fabro_util::text::strip_goal_decoration; use fabro_workflow::run_status::RunStatus; use crate::args::{GlobalArgs, RunsListArgs}; +use crate::command_context::CommandContext; use crate::server_runs::{ServerSummaryLookup, filter_server_runs}; use crate::shared::{color_if, format_duration_ms, tilde_path}; @@ -21,7 +22,8 @@ pub(crate) async fn list_command( styles: &Styles, globals: &GlobalArgs, ) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let label_filters = parse_label_filters(&args.filter.label); let filtered = filter_server_runs( lookup.runs(), diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index 36e435fe2..3d488e61e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result, bail}; use crate::args::{GlobalArgs, RunsRemoveArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::server_runs::{ ServerRunSummaryInfo, ServerSummaryLookup, resolve_server_run_from_summaries, @@ -10,7 +11,8 @@ use crate::shared::print_json_pretty; use super::short_run_id; pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs) -> Result<()> { - let lookup = ServerSummaryLookup::connect(&args.server).await?; + let ctx = CommandContext::for_target(&args.server)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; remove_from(args, lookup.client(), lookup.runs(), globals).await } diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index a599e2d58..ad2dcf879 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -2,6 +2,7 @@ use anyhow::Result; use fabro_api::Client; use crate::args::{GlobalArgs, SecretListArgs}; +use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn list_command( @@ -13,7 +14,7 @@ pub(super) async fn list_command( .list_secrets() .send() .await - .map_err(super::map_api_error)?; + .map_err(server_client::map_api_error)?; let secrets = response.into_inner().data; if globals.json { print_json_pretty(&secrets)?; diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index f91b692dd..fde139022 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -2,43 +2,17 @@ mod list; mod rm; mod set; -use anyhow::{Result, anyhow}; +use anyhow::Result; use crate::args::{GlobalArgs, SecretCommand, SecretNamespace}; -use crate::server_client; - -fn map_api_error(err: progenitor_client::Error) -> anyhow::Error -where - E: serde::Serialize + std::fmt::Debug, -{ - match err { - progenitor_client::Error::ErrorResponse(response) => { - let status = response.status(); - if let Ok(value) = serde_json::to_value(response.into_inner()) { - if let Some(detail) = value - .get("errors") - .and_then(serde_json::Value::as_array) - .and_then(|errors| errors.first()) - .and_then(|entry| entry.get("detail")) - .and_then(serde_json::Value::as_str) - { - return anyhow!("{detail}"); - } - } - anyhow!("request failed with status {status}") - } - progenitor_client::Error::UnexpectedResponse(response) => { - anyhow!("request failed with status {}", response.status()) - } - other => anyhow!("{other}"), - } -} +use crate::command_context::CommandContext; pub(crate) async fn dispatch(ns: SecretNamespace, globals: &GlobalArgs) -> Result<()> { - let client = server_client::connect_server_backed_api_client(&ns.target).await?; + let ctx = CommandContext::for_target(&ns.target)?; + let server = ctx.server().await?; match ns.command { - SecretCommand::List(args) => list::list_command(&client, &args, globals).await, - SecretCommand::Rm(args) => rm::rm_command(&client, &args, globals).await, - SecretCommand::Set(args) => set::set_command(&client, &args, globals).await, + SecretCommand::List(args) => list::list_command(server.api(), &args, globals).await, + SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, globals).await, + SecretCommand::Set(args) => set::set_command(server.api(), &args, globals).await, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 2f3f948c6..376abe5a6 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -2,6 +2,7 @@ use anyhow::Result; use fabro_api::Client; use crate::args::{GlobalArgs, SecretRmArgs}; +use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn rm_command( @@ -14,7 +15,7 @@ pub(super) async fn rm_command( .name(args.key.clone()) .send() .await - .map_err(super::map_api_error)?; + .map_err(server_client::map_api_error)?; if globals.json { print_json_pretty(&serde_json::json!({ "key": args.key }))?; } else { diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index da04d10d9..9ea94ab5b 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -2,6 +2,7 @@ use anyhow::Result; use fabro_api::{Client, types}; use crate::args::{GlobalArgs, SecretSetArgs}; +use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn set_command( @@ -17,7 +18,7 @@ pub(super) async fn set_command( }) .send() .await - .map_err(super::map_api_error)? + .map_err(server_client::map_api_error)? .into_inner(); if globals.json { print_json_pretty(&meta)?; diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index 9defef074..5c970336a 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -5,16 +5,15 @@ use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; use crate::args::{DfArgs, GlobalArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::shared::{format_size, print_json_pretty}; pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> { - let client = server_client::connect_server_backed_api_client_with_storage_dir( - &args.connection.target, - args.connection.storage_dir.as_deref(), - ) - .await?; - let output = client + let ctx = CommandContext::for_connection(&args.connection)?; + let server = ctx.server().await?; + let output = server + .api() .get_system_disk_usage() .verbose(args.verbose) .send() @@ -25,7 +24,8 @@ pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<() let storage_dir = if globals.json { None } else { - client + server + .api() .get_system_info() .send() .await diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index 0285fc4e4..096eddb4c 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -2,17 +2,15 @@ use anyhow::Result; use futures::StreamExt; use crate::args::{GlobalArgs, SystemEventsArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::sse; pub(super) async fn events_command(args: &SystemEventsArgs, globals: &GlobalArgs) -> Result<()> { - let client = server_client::connect_server_backed_api_client_with_storage_dir( - &args.connection.target, - args.connection.storage_dir.as_deref(), - ) - .await?; + let ctx = CommandContext::for_connection(&args.connection)?; + let server = ctx.server().await?; - let mut request = client.attach_events(); + let mut request = server.api().attach_events(); if !args.run_ids.is_empty() { request = request.run_id(args.run_ids.join(",")); } diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index e0a4b9003..23934d6a4 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,16 +1,15 @@ use anyhow::Result; use crate::args::{GlobalArgs, SystemInfoArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn info_command(args: &SystemInfoArgs, globals: &GlobalArgs) -> Result<()> { - let client = server_client::connect_server_backed_api_client_with_storage_dir( - &args.connection.target, - args.connection.storage_dir.as_deref(), - ) - .await?; - let response = client + let ctx = CommandContext::for_connection(&args.connection)?; + let server = ctx.server().await?; + let response = server + .api() .get_system_info() .send() .await diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index b86329751..98bab80ef 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -6,16 +6,15 @@ use tracing::{debug, info}; use fabro_api::types; use crate::args::{GlobalArgs, RunsPruneArgs}; +use crate::command_context::CommandContext; use crate::server_client; use crate::shared::{format_size, print_json_pretty}; pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> { - let client = server_client::connect_server_backed_api_client_with_storage_dir( - &args.connection.target, - args.connection.storage_dir.as_deref(), - ) - .await?; - let response = client + let ctx = CommandContext::for_connection(&args.connection)?; + let server = ctx.server().await?; + let response = server + .api() .prune_runs() .body(types::PruneRunsRequest { before: args.filter.before.clone(), diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index 6050de7b8..fe1a7fab6 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -3,9 +3,9 @@ use fabro_config::ConfigLayer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, ValidateArgs}; +use crate::command_context::CommandContext; use crate::commands::run::output::api_diagnostics_to_local; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest}; -use crate::server_client; use crate::shared::{print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( @@ -13,15 +13,15 @@ pub(crate) async fn run( styles: &Styles, globals: &GlobalArgs, ) -> anyhow::Result<()> { - let cwd = std::env::current_dir()?; + let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), - cwd, + cwd: ctx.cwd().to_path_buf(), args_layer: ConfigLayer::default(), args: None, run_id: None, })?; - let client = server_client::connect_server_only(&args.target).await?; + let client = ctx.server().await?; let response = client.run_preflight(built.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 5fb95fbd9..37a453d35 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -1,6 +1,7 @@ #![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] mod args; +mod command_context; mod commands; mod logging; mod manifest_builder; diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index 14b86167e..410331208 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -97,11 +97,14 @@ pub(crate) async fn connect_server_target_direct(target: &str) -> Result Result { - let settings = user_config::load_settings()?; - let target = user_config::resolve_server_target(args, &settings)?; +pub(crate) async fn connect_server_with_settings( + args: &ServerTargetArgs, + settings: &Settings, + base_config_path: &Path, +) -> Result { + let target = user_config::resolve_server_target(args, settings)?; let runtime = LocalServerRuntime { - active_config_path: user_config::active_settings_path(None), + active_config_path: base_config_path.to_path_buf(), storage_dir: settings.storage_dir(), }; connect_target_api_client_bundle(&target, &runtime).await @@ -125,15 +128,6 @@ pub(crate) async fn connect_api_client(storage_dir: &Path) -> Result Result { - connect_target_api_client_bundle(target, runtime) - .await - .map(|client| client.client) -} - async fn connect_target_api_client_bundle( target: &user_config::ServerTarget, runtime: &LocalServerRuntime, @@ -158,25 +152,6 @@ async fn connect_target_api_client_bundle( } } -pub(crate) async fn connect_server_backed_api_client( - args: &ServerTargetArgs, -) -> Result { - connect_server_backed_api_client_with_storage_dir(args, None).await -} - -pub(crate) async fn connect_server_backed_api_client_with_storage_dir( - args: &ServerTargetArgs, - storage_dir: Option<&Path>, -) -> Result { - let settings = user_config::load_settings_with_storage_dir(storage_dir)?; - let target = user_config::resolve_server_target(args, &settings)?; - let runtime = LocalServerRuntime { - active_config_path: user_config::active_settings_path(None), - storage_dir: settings.storage_dir(), - }; - connect_target_api_client(&target, &runtime).await -} - fn connect_remote_api_client_bundle( api_url: &str, tls: Option<&user_config::ClientTlsSettings>, @@ -288,6 +263,20 @@ impl ServerStoreClient { self.clone() } + pub(crate) fn api(&self) -> &fabro_api::Client { + &self.client + } + + #[allow(dead_code)] + pub(crate) fn http_client(&self) -> &reqwest::Client { + &self.http_client + } + + #[allow(dead_code)] + pub(crate) fn base_url(&self) -> &str { + &self.base_url + } + pub(crate) async fn retrieve_server_settings(&self) -> Result { let response = self .client diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index 344b01098..c1c67b75a 100644 --- a/lib/crates/fabro-cli/src/server_runs.rs +++ b/lib/crates/fabro-cli/src/server_runs.rs @@ -1,4 +1,5 @@ use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::collections::HashMap; @@ -8,7 +9,6 @@ use fabro_store::RunSummary; use fabro_types::{RunId, RunStatus, StatusReason}; use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, scratch_base}; -use crate::args::ServerTargetArgs; use crate::server_client::{self, ServerStoreClient}; pub(crate) struct ServerRunLookup { @@ -105,13 +105,12 @@ impl ServerRunSummaryInfo { } pub(crate) struct ServerSummaryLookup { - client: ServerStoreClient, + client: Arc, runs: Vec, } impl ServerSummaryLookup { - pub(crate) async fn connect(args: &ServerTargetArgs) -> Result { - let client = server_client::connect_server_only(args).await?; + pub(crate) async fn from_client(client: Arc) -> Result { let summaries = client.list_store_runs().await?; let mut runs = summaries .into_iter() @@ -126,7 +125,7 @@ impl ServerSummaryLookup { } pub(crate) fn client(&self) -> &ServerStoreClient { - &self.client + self.client.as_ref() } pub(crate) fn runs(&self) -> &[ServerRunSummaryInfo] {