| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- //! Global config management: path resolution, `config path` display,
- //! `config init-global` scaffold.
- use crate::config::default_config_dir;
- use std::path::PathBuf;
- /// Global config template written by `config init-global`.
- const GLOBAL_TEMPLATE: &str = r#"# Global sm121 config (created by `sm121 config init-global`).
- # Used when ./config.toml is absent, so `sm121` works from any cwd.
- # `ssh_user` / `ssh_key_path` may instead be provided by a cluster source.
- # `~` is expanded.
- #
- # recipe_sources = ["/path/to/recipes"] # extra recipe dirs
- # cluster_sources = ["/path/to/clusters.toml"] # extra cluster def files
- "#;
- /// Resolve the path to the config file.
- ///
- /// Search order:
- /// 1. `--config <path>` from CLI (always wins; error if missing)
- /// 2. `./config.toml` in cwd
- /// 3. `<default_config_dir>/config.toml`
- ///
- /// Returns the explicit path if nothing is found; the loader reports a
- /// clean error.
- pub fn resolve_config_path(cli_config: &str) -> PathBuf {
- let explicit = PathBuf::from(cli_config);
- // User passed a non-default path: use it as-is.
- if cli_config != "config.toml" {
- return explicit;
- }
- // Default value (or no flag): try cwd first, then default folder.
- if explicit.exists() {
- return explicit;
- }
- if let Some(dir) = default_config_dir() {
- let candidate = dir.join("config.toml");
- if candidate.exists() {
- return candidate;
- }
- }
- explicit
- }
- /// Resolve + read the config in one step. A missing file must not surface
- /// as a bare "No such file or directory (os error 2)" — the error names
- /// every location that was searched.
- pub async fn read_config(cli_config: &str) -> anyhow::Result<(PathBuf, String)> {
- let path = resolve_config_path(cli_config);
- match tokio::fs::read_to_string(&path).await {
- Ok(raw) => Ok((path, raw)),
- Err(e) => {
- let msg = if path == std::path::Path::new("config.toml") {
- // resolve_config_path's "found nowhere" sentinel
- let mut m = String::from("config not found; searched: ./config.toml");
- if let Some(dir) = default_config_dir() {
- m.push_str(&format!(", {}/config.toml", dir.display()));
- }
- m.push_str(" — pass --config <path> or run `sm121 config init-global`");
- m
- } else {
- format!("cannot read config {}", path.display())
- };
- anyhow::bail!("{msg} ({e})")
- }
- }
- }
- /// Entry point for `sm121 config ...` subcommands (dispatched before
- /// cluster_sources merging so they work even with broken sources).
- pub fn run(
- cmd: &crate::cli::ConfigCommands,
- cli_config: &str,
- dry_run: bool,
- ) -> anyhow::Result<()> {
- match cmd {
- crate::cli::ConfigCommands::Path => show_paths(cli_config),
- crate::cli::ConfigCommands::InitGlobal => init_global(dry_run)?,
- }
- Ok(())
- }
- /// `sm121 config path`: print the active config and the global config
- /// location (the part that is not transparent — it lives in the
- /// platform config dir, e.g. `~/Library/Application Support/sm121` on macOS).
- pub fn show_paths(cli_config: &str) {
- let active = resolve_config_path(cli_config);
- let origin = if cli_config != "config.toml" {
- "--config"
- } else if std::path::Path::new("config.toml").exists() {
- "./config.toml (cwd)"
- } else {
- "global config dir"
- };
- println!("active config: {} [{}]", active.display(), origin);
- if let Some(dir) = default_config_dir() {
- let global = dir.join("config.toml");
- let state = if global.exists() {
- "exists"
- } else {
- "missing — `sm121 config init-global` creates a template"
- };
- println!("global config: {} [{}]", global.display(), state);
- }
- }
- /// `sm121 config init-global`: create the global config (template) if
- /// missing. Never overwrites an existing file.
- pub fn init_global(dry_run: bool) -> anyhow::Result<()> {
- let dir = default_config_dir().ok_or_else(|| {
- anyhow::anyhow!("cannot determine the platform config dir (dirs::config_dir)")
- })?;
- let global = dir.join("config.toml");
- if global.exists() {
- println!("global config already exists: {}", global.display());
- return Ok(());
- }
- if dry_run {
- println!("would write: {}", global.display());
- return Ok(());
- }
- std::fs::create_dir_all(&dir)?;
- std::fs::write(&global, GLOBAL_TEMPLATE)?;
- println!("created global config template: {}", global.display());
- Ok(())
- }
- #[cfg(test)]
- mod tests {
- use super::resolve_config_path;
- #[tokio::test]
- async fn missing_config_error_names_the_paths_searched() {
- let err = super::read_config("/nonexistent/sm121-config.toml")
- .await
- .unwrap_err()
- .to_string();
- assert!(err.contains("/nonexistent/sm121-config.toml"), "{err}");
- }
- #[test]
- fn explicit_config_path_wins() {
- assert_eq!(
- resolve_config_path("/tmp/explicit-sm121.toml"),
- std::path::PathBuf::from("/tmp/explicit-sm121.toml")
- );
- }
- #[test]
- fn default_config_dir_is_some() {
- assert!(crate::config::default_config_dir().is_some());
- }
- }
|