use std::path::PathBuf;
use anyhow::{Context, anyhow};
use clap::Subcommand;
use tokio::fs;
use crate::project::CONFIG_FILE_NAME;
#[derive(Clone, Subcommand)]
pub enum ConfigCommand {
Init,
Set {
key: String,
value: String,
},
Get {
key: String,
},
Show,
}
pub async fn handle(command: ConfigCommand) -> anyhow::Result<()> {
match command {
ConfigCommand::Init => handle_init().await,
ConfigCommand::Set { key, value } => handle_set(&key, &value).await,
ConfigCommand::Get { key } => handle_get(&key).await,
ConfigCommand::Show => handle_show().await,
}
}
async fn handle_init() -> anyhow::Result<()> {
let config_path = resolve_config_path()?;
if config_path.exists() {
println!("ℹ️ {} already exists at {}", CONFIG_FILE_NAME, config_path.display());
return Ok(());
}
let default = toml::to_string_pretty(&crate::project::ProjectConfig::default())?;
fs::write(&config_path, &default).await.context("writing config file")?;
println!("✅ Created {} at {}", CONFIG_FILE_NAME, config_path.display());
Ok(())
}
async fn handle_set(key: &str, value: &str) -> anyhow::Result<()> {
let config_path = resolve_config_path()?;
if !config_path.exists() {
let default = toml::to_string_pretty(&crate::project::ProjectConfig::default())?;
fs::write(&config_path, &default)
.await
.context("creating config file")?;
println!("✅ Created {} at {}", CONFIG_FILE_NAME, config_path.display());
}
let content = fs::read_to_string(&config_path).await.context("reading config")?;
let mut doc = content.parse::<toml_edit::DocumentMut>().context("parsing config")?;
set_dotted_key(&mut doc, key, value)?;
fs::write(&config_path, doc.to_string())
.await
.context("writing config")?;
println!("✅ Set {key} = {value}");
Ok(())
}
async fn handle_get(key: &str) -> anyhow::Result<()> {
let config_path = find_existing_config()?;
let content = fs::read_to_string(&config_path).await.context("reading config")?;
let doc = content.parse::<toml_edit::DocumentMut>().context("parsing config")?;
let value = get_dotted_key(&doc, key)?;
println!("{value}");
Ok(())
}
async fn handle_show() -> anyhow::Result<()> {
let config_path = find_existing_config()?;
let content = fs::read_to_string(&config_path).await.context("reading config")?;
println!("# {}\n", config_path.display());
print!("{content}");
Ok(())
}
pub fn resolve_config_path() -> anyhow::Result<PathBuf> {
let cwd = std::env::current_dir()?;
let root = if cwd.join("Cargo.toml").exists() {
cwd
} else {
find_repo_root().unwrap_or(cwd)
};
Ok(root.join(CONFIG_FILE_NAME))
}
fn find_existing_config() -> anyhow::Result<PathBuf> {
let mut dir = std::env::current_dir()?;
loop {
let candidate = dir.join(CONFIG_FILE_NAME);
if candidate.exists() {
return Ok(candidate);
}
if !dir.pop() {
break;
}
}
Err(anyhow!(
"No {} found. Run `tari config init` to create one.",
CONFIG_FILE_NAME
))
}
pub fn find_repo_root() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
if dir.join(".git").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
pub fn set_dotted_key(doc: &mut toml_edit::DocumentMut, key: &str, value: &str) -> anyhow::Result<()> {
let parts: Vec<&str> = key.split('.').collect();
if parts.is_empty() || parts.iter().any(|p| p.is_empty()) {
return Err(anyhow!("Empty or malformed key: '{key}'"));
}
if parts.len() == 1 {
doc.insert(parts[0], toml_edit::value(value));
return Ok(());
}
let (leaf, head) = parts.split_last().expect("non-empty parts");
let root = doc
.entry(head[0])
.or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
.as_table_mut()
.ok_or_else(|| anyhow!("'{}' is not a table", head[0]))?;
if head.len() > 1 {
root.set_implicit(true);
}
let last_idx = head.len().saturating_sub(2); let mut table = root;
for (i, part) in head[1..].iter().enumerate() {
let entry = table
.entry(part)
.or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()));
table = entry.as_table_mut().ok_or_else(|| anyhow!("'{part}' is not a table"))?;
if i < last_idx {
table.set_implicit(true);
}
}
table.insert(leaf, toml_edit::value(value));
Ok(())
}
fn get_dotted_key(doc: &toml_edit::DocumentMut, key: &str) -> anyhow::Result<String> {
let parts: Vec<&str> = key.split('.').collect();
if parts.is_empty() {
return Err(anyhow!("Empty key"));
}
let mut item: Option<&toml_edit::Item> = doc.get(parts[0]);
for part in &parts[1..] {
item = item.and_then(|i| i.get(part));
}
match item {
Some(toml_edit::Item::Value(v)) => {
let s = v.to_string();
Ok(s.trim().trim_matches('"').to_string())
},
Some(toml_edit::Item::Table(t)) => Ok(t.to_string()),
Some(other) => Ok(other.to_string()),
None => Err(anyhow!("Key '{key}' not found")),
}
}