use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[cfg(doc)]
use super::Config;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArrayMergeStrategy {
#[default]
Replace,
Append,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct MergeConfig {
#[serde(default)]
pub arrays: ArrayMergeStrategy,
}
pub(crate) fn home_dir() -> Option<PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
}
pub(crate) fn merge_toml(base: &mut toml::Value, overlay: toml::Value, arrays: ArrayMergeStrategy) {
match (base, overlay) {
(toml::Value::Table(base_tbl), toml::Value::Table(overlay_tbl)) => {
for (key, val) in overlay_tbl {
match base_tbl.get_mut(&key) {
Some(existing) => merge_toml(existing, val, arrays),
None => {
base_tbl.insert(key, val);
}
}
}
}
(toml::Value::Array(base_arr), toml::Value::Array(overlay_arr))
if arrays == ArrayMergeStrategy::Append =>
{
base_arr.extend(overlay_arr);
}
(slot, overlay) => *slot = overlay,
}
}
pub(crate) const CONTROL_PLANE_KEYS: &[&str] = &[
"providers", "lifecycle", "shell", "backends", "default_backend", "discovery", "dgx", "scratch", "network",
"crews",
"loadouts",
"tui",
];
pub(crate) fn strip_control_plane(value: &mut toml::Value) {
if let Some(table) = value.as_table_mut() {
for key in CONTROL_PLANE_KEYS {
table.remove(*key);
}
}
}
pub(crate) fn merge_project_overlay(
base: &mut toml::Value,
mut overlay: toml::Value,
arrays: ArrayMergeStrategy,
) {
strip_control_plane(&mut overlay);
merge_toml(base, overlay, arrays);
}
pub(super) fn mark_project_mcp_untrusted(
servers: &mut [crate::mcp::McpServerEntry],
strategy: ArrayMergeStrategy,
project_mcp_count: Option<usize>,
) {
let Some(count) = project_mcp_count else {
return;
};
let start = match strategy {
ArrayMergeStrategy::Replace => 0,
ArrayMergeStrategy::Append => servers.len().saturating_sub(count),
};
for entry in &mut servers[start..] {
entry.trust = crate::mcp::McpTrust::Untrusted;
}
}
pub(super) fn base_is_ambient_newt_toml(base: Option<&Path>) -> bool {
let ambient_candidate = Path::new("./newt.toml");
if base != Some(ambient_candidate) {
return false;
}
match std::env::var("NEWT_CONFIG") {
Ok(explicit) => Path::new(&explicit) != ambient_candidate,
Err(_) => true,
}
}
pub(super) fn array_merge_strategy(
project: &toml::Value,
base: &toml::Value,
) -> ArrayMergeStrategy {
read_array_strategy(project)
.or_else(|| read_array_strategy(base))
.unwrap_or_default()
}
fn read_array_strategy(value: &toml::Value) -> Option<ArrayMergeStrategy> {
match value.get("merge")?.get("arrays")?.as_str()? {
"append" => Some(ArrayMergeStrategy::Append),
"replace" => Some(ArrayMergeStrategy::Replace),
_ => None,
}
}
pub(crate) fn find_project_config_from(start: &Path, home: Option<&Path>) -> Option<PathBuf> {
find_project_file_from(start, home, "config.toml")
}
pub(crate) fn find_project_file_from(
start: &Path,
home: Option<&Path>,
file: &str,
) -> Option<PathBuf> {
let resolve = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let home = home.map(resolve);
let mut dir = Some(start);
while let Some(current) = dir {
if home.is_some() && home == Some(resolve(current)) {
break;
}
let candidate = current.join(".newt").join(file);
if candidate.is_file() {
return Some(candidate);
}
dir = current.parent();
}
None
}
pub(crate) fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = home_dir() {
return home.join(rest);
}
} else if path == "~" {
if let Some(home) = home_dir() {
return home;
}
}
PathBuf::from(path)
}
pub(crate) fn find_ancestor_dir(
start: &Path,
rel: &Path,
exists: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
for ancestor in start.ancestors() {
let candidate = ancestor.join(rel);
if exists(&candidate) {
return Some(candidate);
}
}
None
}