saya_cli/config/
runtime.rs1use super::sources::Paths;
2use crate::cli::GlobalOptions;
3use saya_config::{CliOverrides, ConnectionsFile, ResolutionInput, resolve};
4use std::{
5 collections::BTreeMap,
6 path::{Path, PathBuf},
7};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum RuntimeError {
12 #[error("{0}")]
13 Config(#[from] saya_config::ConfigError),
14 #[error("could not read {path}: {source}")]
15 Read {
16 path: PathBuf,
17 source: std::io::Error,
18 },
19 #[error("explicit path does not exist: {0}")]
20 Missing(PathBuf),
21 #[error("invalid approval mode: {0}")]
22 Approval(String),
23}
24
25#[derive(Clone)]
26pub struct RuntimeConfig {
27 pub resolved: saya_config::ResolvedConfig,
28 pub connections: ConnectionsFile,
29 pub config_path: Option<PathBuf>,
30 pub connections_path: Option<PathBuf>,
31 pub cache_scope: PathBuf,
32 pub(crate) secret_values: BTreeMap<String, String>,
33}
34
35pub fn load(options: &GlobalOptions, cwd: &Path) -> Result<RuntimeConfig, RuntimeError> {
36 load_with_sources(
37 options,
38 cwd,
39 &super::sources::user_config_dir(),
40 super::sources::process_env(),
41 )
42}
43
44pub fn load_with_sources(
45 options: &GlobalOptions,
46 cwd: &Path,
47 user_dir: &Path,
48 process: BTreeMap<String, String>,
49) -> Result<RuntimeConfig, RuntimeError> {
50 let paths = Paths::discover(cwd, user_dir);
51 let user = super::sources::read_config(&paths.user_config)?;
52 let selected_config = options
53 .config
54 .as_ref()
55 .or_else(|| paths.project_config.as_ref().filter(|path| path.exists()))
56 .or_else(|| paths.user_config.as_ref().filter(|path| path.exists()));
57 let project = match options.config.as_ref() {
58 Some(path) => Some(super::sources::read_required_config(path)?),
59 None => super::sources::read_config(&paths.project_config)?,
60 };
61 let selected_connections = options
62 .connections
63 .as_ref()
64 .or_else(|| {
65 paths
66 .project_connections
67 .as_ref()
68 .filter(|path| path.exists())
69 })
70 .or_else(|| paths.user_connections.as_ref().filter(|path| path.exists()));
71 let connections =
72 super::sources::read_connections(selected_connections, options.connections.is_some())?;
73 let env_file = match options.env_file.as_ref() {
74 Some(path) => super::sources::read_env_file(path)?,
75 None => BTreeMap::new(),
76 };
77 let mut secret_values = env_file.clone();
78 secret_values.extend(process.clone());
79 let input = ResolutionInput::new(connections.clone())
80 .with_user(user.unwrap_or_default())
81 .with_project(project.unwrap_or_default())
82 .with_env_file(env_file)
83 .with_process_env(process)
84 .with_cli(CliOverrides {
85 profile: options.profile.clone(),
86 allow_data_sharing: options.allow_data_sharing.then_some(true),
87 ..Default::default()
88 });
89 let cache_scope = super::scope::resolve(selected_connections, cwd);
90 Ok(RuntimeConfig {
91 resolved: resolve(input)?,
92 connections,
93 config_path: selected_config.cloned(),
94 connections_path: selected_connections.cloned(),
95 cache_scope,
96 secret_values,
97 })
98}
99
100impl std::fmt::Debug for RuntimeConfig {
101 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 formatter
103 .debug_struct("RuntimeConfig")
104 .field("resolved", &self.resolved)
105 .field("connections", &self.connections)
106 .field("config_path", &self.config_path)
107 .field("connections_path", &self.connections_path)
108 .field("cache_scope", &"[redacted]")
109 .field("secret_values", &"[redacted]")
110 .finish()
111 }
112}
113
114pub fn approval_mode(options: &GlobalOptions) -> Result<saya_agent::ApprovalPolicy, RuntimeError> {
115 let value = match options.approval_mode.as_deref() {
116 Some(value) => value,
117 None if options.non_interactive => "never",
118 None => "ask",
119 };
120 value
121 .parse()
122 .map_err(|error: saya_agent::ApprovalPolicyParseError| {
123 RuntimeError::Approval(error.to_string())
124 })
125}
126
127pub fn approval_name(options: &GlobalOptions) -> Result<String, RuntimeError> {
128 Ok(match approval_mode(options)? {
129 saya_agent::ApprovalPolicy::Ask => "ask",
130 saya_agent::ApprovalPolicy::ReadOnly => "read-only",
131 saya_agent::ApprovalPolicy::Never => "never",
132 }
133 .into())
134}
135
136pub fn format_name(
137 options: &GlobalOptions,
138 resolved: &saya_config::ResolvedConfig,
139) -> crate::render::RenderFormat {
140 if options.format != crate::cli::FormatArg::Text {
141 options.format.into()
142 } else {
143 resolved.output_format.into()
144 }
145}