use super::{
Location, NpmrcEdit, aube_config, is_npm_shared_key, resolve_aliases, setting_for_key,
};
use clap::Args;
use miette::miette;
#[derive(Debug, Args)]
pub struct SetArgs {
pub key: String,
pub value: String,
#[arg(long, conflicts_with = "location")]
pub local: bool,
#[arg(long, value_enum, default_value_t = Location::User)]
pub location: Location,
}
impl SetArgs {
fn effective_location(&self) -> Location {
if self.local {
Location::Project
} else {
self.location
}
}
}
pub fn run(args: SetArgs) -> miette::Result<()> {
set_value(&args.key, &args.value, args.effective_location(), true)
}
pub(super) fn set_value(
key: &str,
value: &str,
location: Location,
report: bool,
) -> miette::Result<()> {
if is_npm_shared_key(key) {
return write_npmrc(key, value, location, report);
}
if let Some(handled) = try_set_aube_map_entry(key, value, location, report)? {
return Ok(handled);
}
if let Some(meta) = setting_for_key(key)
&& meta.type_ == "object"
{
return Err(reject_aube_map_key(key, meta));
}
reject_scalar_nested_key(key)?;
if let Some(meta) = aube_config::is_aube_config_key(key) {
let path = aube_config_target(location, meta)?;
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
&& let Some(yaml_key) = aube_config::preferred_workspace_yaml_key(meta)
{
aube_config::set_workspace_yaml_value(&path, meta, yaml_key, value)?;
if report {
eprintln!("set {}={} ({})", yaml_key, value, path.display());
}
return Ok(());
}
let mut edit = aube_config::AubeConfigEdit::load(&path)?;
edit.set(meta, value)?;
edit.save(&path)?;
if report {
eprintln!("set {}={} ({})", meta.name, value, path.display());
}
return Ok(());
}
let path = unknown_aube_config_target(location)?;
let mut edit = aube_config::AubeConfigEdit::load(&path)?;
edit.set_unknown(key, value);
edit.save(&path)?;
if report {
eprintln!("set {}={} ({})", key, value, path.display());
}
Ok(())
}
fn try_set_aube_map_entry(
key: &str,
value: &str,
location: Location,
report: bool,
) -> miette::Result<Option<()>> {
let Some((prefix, entry)) = key.split_once('.') else {
return Ok(None);
};
let Some(meta) = setting_for_key(prefix) else {
return Ok(None);
};
if meta.type_ != "object" {
return Ok(None);
}
if aube_config::is_aube_config_key(key).is_some() {
return Ok(None);
}
if !matches!(location, Location::Project) {
return Err(miette!(
code = aube_codes::errors::ERR_AUBE_CONFIG_NESTED_AUBE_KEY,
help = format!(
"use `aube config set --local {prefix}.{entry} {value}` to edit `pnpm-workspace.yaml#{prefix}.{entry}` (or `package.json#aube.{prefix}.{entry}` if no workspace yaml exists). Aube doesn't read user-scope `{prefix}` today.",
),
"`{key}` only applies at project scope: `{prefix}` is read from `pnpm-workspace.yaml` / `package.json#<pnpm|aube>.{prefix}`, not user-scope aube config."
));
}
let (yaml_value, json_value) = scalar_to_yaml_json(value);
let cwd = crate::dirs::project_root_or_cwd()?;
let written =
aube_manifest::workspace::upsert_map_entry(&cwd, meta.name, entry, yaml_value, json_value)
.map_err(|e| miette!("failed to write {}.{entry}: {e}", meta.name))?;
if report {
eprintln!("set {}.{entry}={value} ({})", meta.name, written.display());
}
Ok(Some(()))
}
fn scalar_to_yaml_json(raw: &str) -> (yaml_serde::Value, serde_json::Value) {
if let Some(b) = aube_settings::parse_bool(raw) {
return (yaml_serde::Value::Bool(b), serde_json::Value::Bool(b));
}
(
yaml_serde::Value::String(raw.to_string()),
serde_json::Value::String(raw.to_string()),
)
}
fn write_npmrc(key: &str, value: &str, location: Location, report: bool) -> miette::Result<()> {
let aliases = resolve_aliases(key);
let write_key = preferred_write_key(key, &aliases);
let path = location.path()?;
let mut edit = NpmrcEdit::load(&path)?;
for alias in &aliases {
if alias != &write_key {
edit.remove(alias);
}
}
edit.set(&write_key, value);
edit.save(&path)?;
if report {
eprintln!("set {}={} ({})", write_key, value, path.display());
}
sweep_stale_aube_config(key, &aliases, location)?;
Ok(())
}
fn sweep_stale_aube_config(
key: &str,
aliases: &[String],
location: Location,
) -> miette::Result<()> {
let Some(meta) = aube_config::is_aube_config_key(key) else {
return Ok(());
};
let config_path = match location {
Location::User | Location::Global => aube_config::user_aube_config_path()?,
Location::Project => {
aube_config::project_aube_config_path(&crate::dirs::project_root_or_cwd()?)
}
};
let mut edit = aube_config::AubeConfigEdit::load(&config_path)?;
let mut sweep: Vec<String> = aliases.to_vec();
if !sweep.iter().any(|s| s == meta.name) {
sweep.push(meta.name.to_string());
}
if edit.remove_aliases(&sweep) {
edit.save(&config_path)?;
}
Ok(())
}
fn reject_aube_map_key(key: &str, meta: &aube_settings::meta::SettingMeta) -> miette::Report {
miette!(
code = aube_codes::errors::ERR_AUBE_CONFIG_NESTED_AUBE_KEY,
help = format!(
"set a single entry with `aube config set --local {key}.<entry> <value>`, or edit `{key}:` directly in `pnpm-workspace.yaml` / `aube.{key}` in `package.json`.",
),
"`{key}` is an aube map setting (type `{}`) and can't be set as a single scalar — set one entry at a time, or edit the map structurally.",
meta.type_,
)
}
fn unknown_aube_config_target(location: Location) -> miette::Result<std::path::PathBuf> {
match location {
Location::User | Location::Global => aube_config::user_aube_config_path(),
Location::Project => Ok(aube_config::project_aube_config_path(
&crate::dirs::project_root_or_cwd()?,
)),
}
}
fn aube_config_target(
location: Location,
meta: &aube_settings::meta::SettingMeta,
) -> miette::Result<std::path::PathBuf> {
match location {
Location::User | Location::Global => aube_config::user_aube_config_path(),
Location::Project => {
let cwd = crate::dirs::project_root_or_cwd()?;
let config_path = aube_config::project_aube_config_path(&cwd);
if !config_path.exists()
&& aube_config::preferred_workspace_yaml_key(meta).is_some()
&& let Some(yaml_path) = aube_manifest::workspace::workspace_yaml_existing(&cwd)
{
return Ok(yaml_path);
}
Ok(config_path)
}
}
}
fn reject_scalar_nested_key(key: &str) -> miette::Result<()> {
let Some((prefix, _)) = key.split_once('.') else {
return Ok(());
};
let Some(meta) = setting_for_key(prefix) else {
return Ok(());
};
if meta.type_ == "object" {
return Ok(());
}
Err(miette!(
code = aube_codes::errors::ERR_AUBE_CONFIG_NESTED_AUBE_KEY,
help = format!(
"`{}` is type `{}` — set it directly with `aube config set {} <value>`.",
meta.name, meta.type_, meta.name,
),
"`{key}` is not a writable config key: `{}` is a scalar aube setting and has no nested namespace.",
meta.name,
))
}
pub(super) fn preferred_write_key(input: &str, aliases: &[String]) -> String {
if aliases.iter().any(|a| a == input) {
return input.to_string();
}
aliases
.first()
.cloned()
.unwrap_or_else(|| input.to_string())
}