Skip to main content

saya_cli/config/
runtime.rs

1use 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}