gconfig.rs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. //! Global config management: path resolution, `config path` display,
  2. //! `config init-global` scaffold.
  3. use crate::config::default_config_dir;
  4. use std::path::PathBuf;
  5. /// Global config template written by `config init-global`.
  6. const GLOBAL_TEMPLATE: &str = r#"# Global sm121 config (created by `sm121 config init-global`).
  7. # Used when ./config.toml is absent, so `sm121` works from any cwd.
  8. # `ssh_user` / `ssh_key_path` may instead be provided by a cluster source.
  9. # `~` is expanded.
  10. #
  11. # recipe_sources = ["/path/to/recipes"] # extra recipe dirs
  12. # cluster_sources = ["/path/to/clusters.toml"] # extra cluster def files
  13. "#;
  14. /// Resolve the path to the config file.
  15. ///
  16. /// Search order:
  17. /// 1. `--config <path>` from CLI (always wins; error if missing)
  18. /// 2. `./config.toml` in cwd
  19. /// 3. `<default_config_dir>/config.toml`
  20. ///
  21. /// Returns the explicit path if nothing is found; the loader reports a
  22. /// clean error.
  23. pub fn resolve_config_path(cli_config: &str) -> PathBuf {
  24. let explicit = PathBuf::from(cli_config);
  25. // User passed a non-default path: use it as-is.
  26. if cli_config != "config.toml" {
  27. return explicit;
  28. }
  29. // Default value (or no flag): try cwd first, then default folder.
  30. if explicit.exists() {
  31. return explicit;
  32. }
  33. if let Some(dir) = default_config_dir() {
  34. let candidate = dir.join("config.toml");
  35. if candidate.exists() {
  36. return candidate;
  37. }
  38. }
  39. explicit
  40. }
  41. /// Resolve + read the config in one step. A missing file must not surface
  42. /// as a bare "No such file or directory (os error 2)" — the error names
  43. /// every location that was searched.
  44. pub async fn read_config(cli_config: &str) -> anyhow::Result<(PathBuf, String)> {
  45. let path = resolve_config_path(cli_config);
  46. match tokio::fs::read_to_string(&path).await {
  47. Ok(raw) => Ok((path, raw)),
  48. Err(e) => {
  49. let msg = if path == std::path::Path::new("config.toml") {
  50. // resolve_config_path's "found nowhere" sentinel
  51. let mut m = String::from("config not found; searched: ./config.toml");
  52. if let Some(dir) = default_config_dir() {
  53. m.push_str(&format!(", {}/config.toml", dir.display()));
  54. }
  55. m.push_str(" — pass --config <path> or run `sm121 config init-global`");
  56. m
  57. } else {
  58. format!("cannot read config {}", path.display())
  59. };
  60. anyhow::bail!("{msg} ({e})")
  61. }
  62. }
  63. }
  64. /// Entry point for `sm121 config ...` subcommands (dispatched before
  65. /// cluster_sources merging so they work even with broken sources).
  66. pub fn run(
  67. cmd: &crate::cli::ConfigCommands,
  68. cli_config: &str,
  69. dry_run: bool,
  70. ) -> anyhow::Result<()> {
  71. match cmd {
  72. crate::cli::ConfigCommands::Path => show_paths(cli_config),
  73. crate::cli::ConfigCommands::InitGlobal => init_global(dry_run)?,
  74. }
  75. Ok(())
  76. }
  77. /// `sm121 config path`: print the active config and the global config
  78. /// location (the part that is not transparent — it lives in the
  79. /// platform config dir, e.g. `~/Library/Application Support/sm121` on macOS).
  80. pub fn show_paths(cli_config: &str) {
  81. let active = resolve_config_path(cli_config);
  82. let origin = if cli_config != "config.toml" {
  83. "--config"
  84. } else if std::path::Path::new("config.toml").exists() {
  85. "./config.toml (cwd)"
  86. } else {
  87. "global config dir"
  88. };
  89. println!("active config: {} [{}]", active.display(), origin);
  90. if let Some(dir) = default_config_dir() {
  91. let global = dir.join("config.toml");
  92. let state = if global.exists() {
  93. "exists"
  94. } else {
  95. "missing — `sm121 config init-global` creates a template"
  96. };
  97. println!("global config: {} [{}]", global.display(), state);
  98. }
  99. }
  100. /// `sm121 config init-global`: create the global config (template) if
  101. /// missing. Never overwrites an existing file.
  102. pub fn init_global(dry_run: bool) -> anyhow::Result<()> {
  103. let dir = default_config_dir().ok_or_else(|| {
  104. anyhow::anyhow!("cannot determine the platform config dir (dirs::config_dir)")
  105. })?;
  106. let global = dir.join("config.toml");
  107. if global.exists() {
  108. println!("global config already exists: {}", global.display());
  109. return Ok(());
  110. }
  111. if dry_run {
  112. println!("would write: {}", global.display());
  113. return Ok(());
  114. }
  115. std::fs::create_dir_all(&dir)?;
  116. std::fs::write(&global, GLOBAL_TEMPLATE)?;
  117. println!("created global config template: {}", global.display());
  118. Ok(())
  119. }
  120. #[cfg(test)]
  121. mod tests {
  122. use super::resolve_config_path;
  123. #[tokio::test]
  124. async fn missing_config_error_names_the_paths_searched() {
  125. let err = super::read_config("/nonexistent/sm121-config.toml")
  126. .await
  127. .unwrap_err()
  128. .to_string();
  129. assert!(err.contains("/nonexistent/sm121-config.toml"), "{err}");
  130. }
  131. #[test]
  132. fn explicit_config_path_wins() {
  133. assert_eq!(
  134. resolve_config_path("/tmp/explicit-sm121.toml"),
  135. std::path::PathBuf::from("/tmp/explicit-sm121.toml")
  136. );
  137. }
  138. #[test]
  139. fn default_config_dir_is_some() {
  140. assert!(crate::config::default_config_dir().is_some());
  141. }
  142. }