upstream-rs 1.18.0

Fetch package updates directly from the source.
Documentation
use crate::application::output::Status;
use crate::{
    application::operations::config_operation::ConfigUpdater,
    application::output,
    services::storage::config_storage::ConfigStorage,
    utils::{pager, static_paths::UpstreamPaths},
};
use anyhow::{Result, anyhow};

pub fn run_set(set_keys: Vec<String>) -> Result<()> {
    if set_keys.is_empty() {
        return Err(anyhow!("At least one configuration assignment is required"));
    }

    let paths = UpstreamPaths::new()?;
    let mut config_storage = ConfigStorage::new(&paths.config.config_file)?;
    let mut config_updater = ConfigUpdater::new(&mut config_storage);

    println!("{}", output::title("Config set"));

    if set_keys.len() > 1 {
        let results = config_updater.set_bulk(&set_keys);
        for applied in &results.applied {
            output::status_line(
                Status::Ok,
                &applied.key,
                format!("set to '{}'", applied.display_value),
            );
        }
        for (key, err) in &results.failures {
            output::status_line(Status::Fail, key, err);
        }
    } else {
        let applied = config_updater.set_key(&set_keys[0])?;
        output::status_line(
            Status::Ok,
            &applied.key,
            format!("set to '{}'", applied.display_value),
        );
    }

    println!("{}", output::success("Configuration saved."));
    Ok(())
}

pub fn run_get(get_keys: Vec<String>) -> Result<()> {
    if get_keys.is_empty() {
        return Err(anyhow!("At least one configuration key is required"));
    }

    let paths = UpstreamPaths::new()?;
    let mut config_storage = ConfigStorage::new(&paths.config.config_file)?;
    let config_updater = ConfigUpdater::new(&mut config_storage);

    println!("{}", output::title("Config get"));

    if get_keys.len() > 1 {
        let results = config_updater.get_bulk(&get_keys);

        if results.values.is_empty() {
            println!("{}", output::warning("No values found."));
        } else {
            for (key, value) in results.values {
                output::kv(&key, value);
            }
        }
        for (key, err) in results.failures {
            output::status_line(Status::Fail, key, err);
        }
    } else {
        let value = config_updater.get_key(&get_keys[0])?;
        output::kv(&get_keys[0], value);
    }

    Ok(())
}

pub fn run_list(show_secrets: bool) -> Result<()> {
    let paths = UpstreamPaths::new()?;
    let config_storage = ConfigStorage::new(&paths.config.config_file)?;

    let flattened = config_storage.get_flattened_config();

    if flattened.is_empty() {
        println!("{}", output::warning("No configuration found."));
        return Ok(());
    }

    let mut keys: Vec<_> = flattened.keys().collect();
    keys.sort();
    let mut config_output = String::new();

    for key in keys {
        if let Some(value) = flattened.get(key) {
            let display_value = format_config_value(key, value, show_secrets);
            config_output.push_str(&format!("  {} = {}\n", key, display_value));
        }
    }

    pager::page_text(Some("Current configuration"), &config_output)?;
    Ok(())
}

fn format_config_value(key: &str, value: &str, show_secrets: bool) -> String {
    if !show_secrets && output::is_sensitive_key(key) {
        return output::redact_secret(value);
    }
    value.to_string()
}

pub fn run_reset() -> Result<()> {
    let paths = UpstreamPaths::new()?;
    let mut config_storage = ConfigStorage::new(&paths.config.config_file)?;

    output::confirm_or_cancel("Reset all configuration to defaults?", false)?;
    config_storage.reset_to_defaults()?;
    println!("{}", output::success("Configuration reset to defaults."));

    Ok(())
}

pub fn run_edit() -> Result<()> {
    let paths = UpstreamPaths::new()?;

    let editor = std::env::var("EDITOR")
        .or_else(|_| std::env::var("VISUAL"))
        .unwrap_or_else(|_| {
            if cfg!(target_os = "windows") {
                "notepad".to_string()
            } else {
                "nano".to_string()
            }
        });

    println!("{}", output::title("Config edit"));
    output::action_note(format!("Opening with {}", editor));

    let status = std::process::Command::new(&editor)
        .arg(&paths.config.config_file)
        .status()?;

    if status.success() {
        println!("{}", output::success("Editor closed."));

        // Validate the config can still be loaded
        match ConfigStorage::new(&paths.config.config_file) {
            Ok(_) => println!("{}", output::success("Configuration is valid.")),
            Err(e) => {
                println!(
                    "{}",
                    output::warning(format!("Configuration may have errors: {}", e))
                );
                output::action_note("Fix manually or run 'upstream config reset'.");
            }
        }
    } else {
        println!("{}", output::warning("Editor exited with error."));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::format_config_value;

    #[test]
    fn config_list_redacts_sensitive_values_by_default() {
        assert_eq!(
            format_config_value("github.api_token", "ghp_abcdefghijklmnopqrstuvwxyz", false),
            "ghp_...wxyz"
        );
        assert_eq!(
            format_config_value("github.api_token", "ghp_abcdefghijklmnopqrstuvwxyz", true),
            "ghp_abcdefghijklmnopqrstuvwxyz"
        );
        assert_eq!(format_config_value("github.enabled", "true", false), "true");
    }
}