Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/aube/src/commands/add/filtered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub(super) async fn run(
&args.packages,
args.allow_low_downloads,
crate::commands::add_supply_chain::LowDownloadPrompt::Terminal,
&[],
)
.await?;

Expand Down
88 changes: 50 additions & 38 deletions crates/aube/src/commands/add/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub(super) struct AddManifestOptions {
/// forces a registry-style spec even when `linkWorkspacePackages`
/// matched a sibling.
pub(super) workspace_protocol_override: Option<bool>,
/// Highest-precedence settings supplied by an embedding host.
pub(super) setting_overrides: Vec<(String, String)>,
}

impl AddManifestOptions {
Expand All @@ -47,6 +49,7 @@ impl AddManifestOptions {
args.save_workspace_protocol,
args.no_save_workspace_protocol,
),
setting_overrides: Vec::new(),
}
}
}
Expand Down Expand Up @@ -89,43 +92,50 @@ pub(super) async fn update_manifest_for_add(
// own dir so a sub-project's `.npmrc` still wins — switching the
// entire context to the workspace root would silently drop those
// overrides, since `load_npmrc_entries` doesn't walk up.
let (default_tag, default_prefix, catalog_mode, minimum_release_age, registry_supports_time) =
crate::commands::with_settings_ctx(cwd, |ctx| {
let tag = aube_settings::resolved::tag(ctx);
let prefix = if opts.save_exact {
String::new()
} else {
let raw = aube_settings::resolved::save_prefix(ctx);
// Validate: only ^, ~, or empty are valid prefixes.
match raw.as_str() {
"^" | "~" | "" => raw,
_ => {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_SAVE_PREFIX,
"ignoring invalid save-prefix={raw:?}, falling back to ^"
);
"^".to_string()
}
let (
default_tag,
default_prefix,
catalog_mode,
minimum_release_age,
registry_supports_time,
cache_dir,
) = crate::commands::with_settings_ctx_and_cli(cwd, &opts.setting_overrides, |ctx| {
let tag = aube_settings::resolved::tag(ctx);
let prefix = if opts.save_exact {
String::new()
} else {
let raw = aube_settings::resolved::save_prefix(ctx);
// Validate: only ^, ~, or empty are valid prefixes.
match raw.as_str() {
"^" | "~" | "" => raw,
_ => {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_SAVE_PREFIX,
"ignoring invalid save-prefix={raw:?}, falling back to ^"
);
"^".to_string()
}
};
let catalog_mode = aube_settings::resolved::catalog_mode(ctx);
// The version this function writes into the manifest must
// honor `minimumReleaseAge` the same way full resolution
// does: dist-tag adds and `--save-exact` pin a concrete
// version here, and a pinned fresh version would sail past
// the resolver's gate via its lenient exact-range fallback.
let minimum_release_age =
crate::commands::install::resolve_minimum_release_age(ctx, None);
let registry_supports_time_field =
aube_settings::resolved::registry_supports_time_field(ctx);
(
tag,
prefix,
catalog_mode,
minimum_release_age,
registry_supports_time_field,
)
});
}
};
let catalog_mode = aube_settings::resolved::catalog_mode(ctx);
// The version this function writes into the manifest must
// honor `minimumReleaseAge` the same way full resolution
// does: dist-tag adds and `--save-exact` pin a concrete
// version here, and a pinned fresh version would sail past
// the resolver's gate via its lenient exact-range fallback.
let minimum_release_age = crate::commands::install::resolve_minimum_release_age(ctx, None);
let registry_supports_time_field =
aube_settings::resolved::registry_supports_time_field(ctx);
let cache_dir = crate::commands::resolved_cache_dir_with_ctx(cwd, ctx);
(
tag,
prefix,
catalog_mode,
minimum_release_age,
registry_supports_time_field,
cache_dir,
)
});
let workspace_settings_cwd = crate::dirs::find_workspace_yaml_root(cwd)
.or_else(|| crate::dirs::find_workspace_root(cwd))
.unwrap_or_else(|| cwd.to_path_buf());
Expand Down Expand Up @@ -247,8 +257,8 @@ pub(super) async fn update_manifest_for_add(
// `registrySupportsTimeField` keeps the cheaper abbreviated path hot
// when the registry inlines `time` in corgi payloads.
let needs_time = minimum_release_age.is_some() && !registry_supports_time;
let corgi_cache_dir = crate::commands::packument_cache_dir_for_cwd(cwd);
let full_cache_dir = crate::commands::packument_full_cache_dir_for_cwd(cwd);
let corgi_cache_dir = cache_dir.join("packuments-v1");
let full_cache_dir = cache_dir.join("packuments-full-v1");
let offline = opts.network_mode == aube_registry::NetworkMode::Offline;
for spec in &parsed {
if aube_util::pkg::is_workspace_spec(&spec.range)
Expand Down Expand Up @@ -1050,6 +1060,7 @@ mod tests {
network_mode: aube_registry::NetworkMode::Offline,
save_catalog: None,
workspace_protocol_override: None,
setting_overrides: Vec::new(),
},
false,
)
Expand Down Expand Up @@ -1108,6 +1119,7 @@ mod tests {
network_mode: aube_registry::NetworkMode::Offline,
save_catalog: None,
workspace_protocol_override: None,
setting_overrides: Vec::new(),
},
false,
)
Expand Down
22 changes: 22 additions & 0 deletions crates/aube/src/commands/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ pub async fn add_to_project(
project_dir: &std::path::Path,
packages: &[String],
options: AddToProjectOptions,
) -> miette::Result<()> {
add_to_project_with_overrides(
project_dir,
packages,
options,
install::EmbedderInstallOverrides::default(),
)
.await
}

pub(crate) async fn add_to_project_with_overrides(
project_dir: &std::path::Path,
packages: &[String],
options: AddToProjectOptions,
overrides: install::EmbedderInstallOverrides,
) -> miette::Result<()> {
if packages.is_empty() {
return Ok(());
Expand All @@ -75,6 +90,8 @@ pub async fn add_to_project(
// Serialize on the same root lock as the CLI add path.
let lock = super::take_install_project_lock(project_dir)?;
options.control.check_cancelled()?;
let mut setting_overrides = Vec::new();
overrides.append_to(&mut setting_overrides)?;
let manifest_path = project_dir.join("package.json");
let original_manifest = std::fs::read(&manifest_path)
.into_diagnostic()
Expand All @@ -95,6 +112,7 @@ pub async fn add_to_project(
crate::commands::add_supply_chain::LowDownloadPrompt::Host(
options.control.clone(),
),
&setting_overrides,
)
.await?;
}
Expand All @@ -113,6 +131,7 @@ pub async fn add_to_project(
},
save_catalog: None,
workspace_protocol_override: None,
setting_overrides: setting_overrides.clone(),
},
false,
)
Expand All @@ -138,6 +157,7 @@ pub async fn add_to_project(
);
install_options.control = options.control;
install_options.embedder_runtime = options.runtime;
install_options.cli_flags.extend(setting_overrides);
if options.offline {
install_options.network_mode = aube_registry::NetworkMode::Offline;
}
Expand Down Expand Up @@ -528,6 +548,7 @@ pub async fn run(
packages,
allow_low_downloads,
crate::commands::add_supply_chain::LowDownloadPrompt::Terminal,
&[],
)
.await?;

Expand All @@ -545,6 +566,7 @@ pub async fn run(
save_workspace_protocol,
no_save_workspace_protocol,
),
setting_overrides: Vec::new(),
},
!no_save,
)
Expand Down
7 changes: 5 additions & 2 deletions crates/aube/src/commands/add/supply_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(super) async fn run_cli_name_gates(
packages: &[String],
allow_low_downloads: bool,
prompt: crate::commands::add_supply_chain::LowDownloadPrompt,
setting_overrides: &[(String, String)],
) -> miette::Result<()> {
let project_dir = supply_chain_project_dir(cwd);
let manifest = crate::commands::load_manifest_or_default(&project_dir)?;
Expand All @@ -18,7 +19,8 @@ pub(super) async fn run_cli_name_gates(
minimum_package_age_minutes,
mut allowed_unpopular,
lockfile_dir,
) = crate::commands::with_settings_ctx(&project_dir, |ctx| {
cache_dir,
) = crate::commands::with_settings_ctx_and_cli(&project_dir, setting_overrides, |ctx| {
let policy = if aube_settings::resolved::paranoid(ctx) {
aube_settings::resolved::AdvisoryCheck::Required
} else {
Expand All @@ -30,6 +32,7 @@ pub(super) async fn run_cli_name_gates(
aube_settings::resolved::minimum_package_age(ctx),
aube_settings::resolved::allowed_unpopular_packages(ctx).unwrap_or_default(),
crate::commands::install::resolve_active_lockfile_dir(&project_dir, &manifest, ctx)?,
crate::commands::resolved_cache_dir_with_ctx(&project_dir, ctx),
))
})?;
let locked_registry_names = locked_registry_names(&lockfile_dir, &manifest);
Expand All @@ -39,7 +42,7 @@ pub(super) async fn run_cli_name_gates(
.map(|name| glob::Pattern::escape(name)),
);
let registry_client = crate::commands::make_client(&project_dir);
let full_packument_cache = crate::commands::packument_full_cache_dir_for_cwd(&project_dir);
let full_packument_cache = cache_dir.join("packuments-full-v1");
crate::commands::add_supply_chain::run_gates(
&registry_inputs.name_only_advisory_names,
&registry_inputs.exact_advisory_pairs,
Expand Down
109 changes: 109 additions & 0 deletions crates/aube/src/commands/install/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,43 @@ impl InstallArgs {
}
}

/// Host-owned setting overrides for one embedded install.
///
/// These values are injected at explicit-command precedence, above process
/// environment variables and project/user configuration, without mutating
/// process-global state. Standalone CLI commands leave this empty.
#[derive(Debug, Clone, Default)]
pub struct EmbedderInstallOverrides {
/// Use aube's shared global virtual store. `Some(false)` materializes
/// packages inside the project so the host owns their complete lifecycle.
pub use_global_virtual_store: Option<bool>,
/// Directory for regenerable metadata caches.
pub cache_dir: Option<std::path::PathBuf>,
/// Root of the content-addressable package store.
pub store_dir: Option<std::path::PathBuf>,
}

impl EmbedderInstallOverrides {
pub(crate) fn append_to(&self, settings: &mut Vec<(String, String)>) -> miette::Result<()> {
if let Some(enabled) = self.use_global_virtual_store {
settings.push(("enableGlobalVirtualStore".to_string(), enabled.to_string()));
}
if let Some(path) = &self.cache_dir {
let path = path
.to_str()
.ok_or_else(|| miette::miette!("embedder cache directory must be valid UTF-8"))?;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
settings.push(("cacheDir".to_string(), path.to_string()));
}
if let Some(path) = &self.store_dir {
let path = path
.to_str()
.ok_or_else(|| miette::miette!("embedder store directory must be valid UTF-8"))?;
settings.push(("storeDir".to_string(), path.to_string()));
}
Ok(())
}
}

/// Aggregated options for `install::run`. Grouped into a struct so we can add
/// more flags (`--no-optional`, `--offline`, etc.) without changing every caller.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -521,3 +558,75 @@ impl From<FrozenMode> for InstallOptions {
Self::with_mode(mode)
}
}

#[cfg(test)]
mod embedder_override_tests {
use super::EmbedderInstallOverrides;

#[test]
fn overrides_beat_environment_and_project_settings() {
let cache_dir = std::path::PathBuf::from("/host/cache");
let store_dir = std::path::PathBuf::from("/host/store");
let overrides = EmbedderInstallOverrides {
use_global_virtual_store: Some(false),
cache_dir: Some(cache_dir.clone()),
store_dir: Some(store_dir.clone()),
};
let mut cli = Vec::new();
overrides.append_to(&mut cli).unwrap();
let env = vec![
(
"npm_config_enable_global_virtual_store".to_string(),
"true".to_string(),
),
("npm_config_cache_dir".to_string(), "/env/cache".to_string()),
("npm_config_store_dir".to_string(), "/env/store".to_string()),
];
let npmrc = vec![
("enableGlobalVirtualStore".to_string(), "true".to_string()),
("cacheDir".to_string(), "/project/cache".to_string()),
("storeDir".to_string(), "/project/store".to_string()),
];
let workspace = std::collections::BTreeMap::new();
let ctx = aube_settings::ResolveCtx {
managed_aube_config: &[],
project_aube_config: &[],
project_npmrc: &npmrc,
user_aube_config: &[],
user_npmrc: &[],
workspace_yaml: &workspace,
env: &env,
cli: &cli,
embedder_defaults: &[],
};

assert_eq!(
aube_settings::resolved::enable_global_virtual_store(&ctx),
Some(false)
);
assert_eq!(
aube_settings::resolved::cache_dir(&ctx),
Some(cache_dir.to_string_lossy().into_owned())
);
assert_eq!(
aube_settings::resolved::store_dir(&ctx),
Some(store_dir.to_string_lossy().into_owned())
);
}

#[cfg(unix)]
#[test]
fn non_utf8_storage_paths_are_rejected() {
use std::os::unix::ffi::OsStringExt;

let overrides = EmbedderInstallOverrides {
cache_dir: Some(std::path::PathBuf::from(std::ffi::OsString::from_vec(
vec![b'/', 0xff],
))),
..Default::default()
};

let error = overrides.append_to(&mut Vec::new()).unwrap_err();
assert!(error.to_string().contains("must be valid UTF-8"));
}
}
Loading
Loading