pub mod add;
pub mod approve_builds;
pub mod audit;
pub mod bin;
pub mod cache;
pub mod cat_file;
pub mod cat_index;
pub mod catalogs;
pub mod check;
pub mod ci;
pub mod clean;
pub mod completion;
pub mod config;
pub mod create;
pub mod dedupe;
pub mod deploy;
pub mod deprecate;
pub mod deprecations;
pub mod diag;
pub mod dist_tag;
pub mod dlx;
pub mod doctor;
pub mod exec;
pub mod fetch;
pub mod find_hash;
pub mod global;
pub mod ignored_builds;
pub mod import;
pub mod init;
pub mod inject;
pub mod install;
pub mod install_test;
pub mod licenses;
pub mod link;
pub mod list;
pub mod login;
pub mod logout;
pub mod npm_fallback;
pub mod npmrc;
pub mod outdated;
pub mod pack;
pub mod patch;
pub mod patch_commit;
pub mod patch_remove;
pub mod peers;
pub mod prune;
pub mod publish;
pub mod publish_provenance;
pub mod query;
pub mod rebuild;
pub mod recursive;
pub mod remove;
pub mod restart;
pub mod root;
pub mod run;
pub mod run_output;
pub mod sbom;
pub mod store;
pub mod undeprecate;
pub mod unlink;
pub mod unpublish;
pub mod update;
pub mod version;
pub mod view;
pub mod why;
use aube_registry::client::RegistryClient;
use aube_registry::config::NpmConfig;
use miette::{Context, IntoDiagnostic, miette};
use std::any::Any;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{OnceLock, RwLock};
static GLOBAL_FROZEN: OnceLock<Option<install::FrozenOverride>> = OnceLock::new();
static GLOBAL_VIRTUAL_STORE: OnceLock<install::GlobalVirtualStoreFlags> = OnceLock::new();
static SKIP_AUTO_INSTALL_ON_PM_MISMATCH: AtomicBool = AtomicBool::new(false);
static REGISTRY_OVERRIDE: RwLock<Option<String>> = RwLock::new(None);
static FETCH_CLI_OVERRIDES: OnceLock<Vec<(String, String)>> = OnceLock::new();
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct GlobalOutputFlags {
pub ndjson: bool,
pub silent: bool,
}
static GLOBAL_OUTPUT: OnceLock<GlobalOutputFlags> = OnceLock::new();
pub(crate) fn set_registry_override(url: Option<String>) {
*REGISTRY_OVERRIDE.write().expect("registry lock poisoned") =
url.map(|u| aube_registry::config::normalize_registry_url_pub(&u));
}
pub(crate) fn set_fetch_cli_overrides(flags: Vec<(String, String)>) {
let _ = FETCH_CLI_OVERRIDES.set(flags);
}
pub(crate) fn fetch_cli_overrides() -> &'static [(String, String)] {
FETCH_CLI_OVERRIDES.get().map(Vec::as_slice).unwrap_or(&[])
}
pub(crate) fn set_skip_auto_install_on_package_manager_mismatch(skip: bool) {
SKIP_AUTO_INSTALL_ON_PM_MISMATCH.store(skip, Ordering::Relaxed);
}
pub(crate) fn skip_auto_install_on_package_manager_mismatch() -> bool {
SKIP_AUTO_INSTALL_ON_PM_MISMATCH.load(Ordering::Relaxed)
}
pub(crate) fn registry_override() -> Option<String> {
REGISTRY_OVERRIDE
.read()
.expect("registry lock poisoned")
.clone()
}
pub(crate) fn load_npm_config(dir: &std::path::Path) -> NpmConfig {
let mut config = NpmConfig::load(dir);
if let Some(url) = registry_override() {
config.registry = url;
}
config
}
pub(crate) fn set_global_frozen_override(flags: Option<install::FrozenOverride>) {
let _ = GLOBAL_FROZEN.set(flags);
}
pub(crate) fn set_global_virtual_store_flags(flags: install::GlobalVirtualStoreFlags) {
let _ = GLOBAL_VIRTUAL_STORE.set(flags);
}
pub(crate) fn set_global_output_flags(flags: GlobalOutputFlags) {
let _ = GLOBAL_OUTPUT.set(flags);
}
pub(crate) fn global_frozen_override() -> Option<install::FrozenOverride> {
GLOBAL_FROZEN.get().copied().unwrap_or_default()
}
pub(crate) fn global_virtual_store_flags() -> install::GlobalVirtualStoreFlags {
GLOBAL_VIRTUAL_STORE.get().copied().unwrap_or_default()
}
pub(crate) fn global_output_flags() -> GlobalOutputFlags {
GLOBAL_OUTPUT.get().copied().unwrap_or_default()
}
pub(crate) fn configure_script_settings(ctx: &aube_settings::ResolveCtx<'_>) {
let node_options = aube_settings::resolved::node_options(ctx).and_then(non_empty_string);
let script_shell = aube_settings::resolved::script_shell(ctx)
.and_then(|s| non_empty_string(s).map(Into::into));
let unsafe_perm = aube_settings::resolved::unsafe_perm(ctx);
let shell_emulator = aube_settings::resolved::shell_emulator(ctx);
aube_scripts::set_script_settings(aube_scripts::ScriptSettings {
node_options,
script_shell,
unsafe_perm,
shell_emulator,
});
}
pub(crate) fn configure_script_settings_for_cwd(cwd: &Path) -> miette::Result<()> {
let files = FileSources::load(cwd);
let (_, raw_workspace) = aube_manifest::workspace::load_both(cwd)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
let env_snapshot = aube_settings::values::capture_env();
let ctx = files.ctx(&raw_workspace, &env_snapshot, &[]);
configure_script_settings(&ctx);
Ok(())
}
pub(crate) struct FileSources {
pub user_npmrc: Vec<(String, String)>,
pub project_npmrc: Vec<(String, String)>,
pub user_aube_config: Vec<(String, String)>,
pub project_aube_config: Vec<(String, String)>,
}
impl FileSources {
pub(crate) fn load(cwd: &Path) -> Self {
let npmrc = aube_registry::config::load_npmrc_entries_split(cwd);
Self {
user_npmrc: npmrc.user,
project_npmrc: npmrc.project,
user_aube_config: config::load_user_aube_config_entries(),
project_aube_config: config::load_project_aube_config_entries(cwd),
}
}
pub(crate) fn ctx<'a>(
&'a self,
workspace_yaml: &'a std::collections::BTreeMap<String, yaml_serde::Value>,
env: &'a [(String, String)],
cli: &'a [(String, String)],
) -> aube_settings::ResolveCtx<'a> {
aube_settings::ResolveCtx {
project_aube_config: &self.project_aube_config,
project_npmrc: &self.project_npmrc,
user_aube_config: &self.user_aube_config,
user_npmrc: &self.user_npmrc,
workspace_yaml,
env,
cli,
}
}
}
fn non_empty_string(value: String) -> Option<String> {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
pub(crate) fn retarget_cwd(path: &Path) -> miette::Result<()> {
let path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().into_diagnostic()?.join(path)
};
std::env::set_current_dir(&path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to chdir into {}", path.display()))?;
crate::dirs::set_cwd(&path)?;
Ok(())
}
pub(crate) fn chained_frozen_mode(default: install::FrozenMode) -> install::FrozenMode {
match global_frozen_override() {
Some(ovr) => install::FrozenMode::from_override(Some(ovr), None),
None => default,
}
}
pub(crate) fn ensure_registry_auth(
client: &RegistryClient,
registry_url: &str,
) -> miette::Result<()> {
if client.has_resolved_auth_for(registry_url) {
Ok(())
} else {
Err(miette!(
"no auth token for {registry_url}. Run `aube login --registry {registry_url}` first."
))
}
}
static LOCK_HELD: AtomicBool = AtomicBool::new(false);
fn aube_no_lock_enabled(cwd: &std::path::Path) -> bool {
with_settings_ctx(cwd, aube_settings::resolved::aube_no_lock)
}
pub(crate) struct ProjectLock {
_inner: Option<Box<dyn Any + Send>>,
owns_flag: bool,
}
impl Drop for ProjectLock {
fn drop(&mut self) {
if self.owns_flag {
LOCK_HELD.store(false, Ordering::Release);
}
}
}
pub(crate) fn take_project_lock(cwd: &std::path::Path) -> miette::Result<ProjectLock> {
if aube_no_lock_enabled(cwd) {
return Ok(ProjectLock {
_inner: None,
owns_flag: false,
});
}
if LOCK_HELD.load(Ordering::Acquire) {
return Ok(ProjectLock {
_inner: None,
owns_flag: false,
});
}
let nm_path = project_modules_dir(cwd);
let lock = xx::fslock::FSLock::new(&nm_path)
.with_callback(|_| {
eprintln!("Waiting for another aube process to finish in this project...");
})
.lock()
.map_err(|e| miette!("failed to acquire project lock: {e}"))?;
LOCK_HELD.store(true, Ordering::Release);
Ok(ProjectLock {
_inner: Some(Box::new(lock)),
owns_flag: true,
})
}
pub(crate) fn open_store(cwd: &std::path::Path) -> miette::Result<aube_store::Store> {
if let Some(custom) = resolved_store_dir(cwd) {
aube_store::Store::with_root(custom.join("v1").join("files"))
.into_diagnostic()
.wrap_err("failed to open store")
} else {
aube_store::Store::default_location()
.into_diagnostic()
.wrap_err("failed to open store")
}
}
fn resolved_store_dir(cwd: &std::path::Path) -> Option<std::path::PathBuf> {
with_settings_ctx(cwd, |ctx| {
let raw = aube_settings::resolved::store_dir(ctx)?;
expand_setting_path(&raw, cwd)
})
}
pub(crate) fn expand_setting_path(raw: &str, cwd: &std::path::Path) -> Option<std::path::PathBuf> {
let expanded = if let Some(rest) = raw.strip_prefix("~/") {
std::path::PathBuf::from(home_dir_os()?).join(rest)
} else if raw == "~" {
std::path::PathBuf::from(home_dir_os()?)
} else {
std::path::PathBuf::from(raw)
};
Some(if expanded.is_absolute() {
expanded
} else {
cwd.join(expanded)
})
}
fn home_dir_os() -> Option<std::ffi::OsString> {
aube_util::env::home_dir().map(|p| p.into_os_string())
}
pub(crate) fn with_settings_ctx<T>(
cwd: &std::path::Path,
f: impl FnOnce(&aube_settings::ResolveCtx<'_>) -> T,
) -> T {
let files = FileSources::load(cwd);
let raw_workspace = aube_manifest::workspace::load_raw(cwd).unwrap_or_default();
let env = aube_settings::values::process_env();
let ctx = files.ctx(&raw_workspace, env, &[]);
f(&ctx)
}
pub(crate) fn make_client(cwd: &std::path::Path) -> aube_registry::client::RegistryClient {
let config = load_npm_config(cwd);
tracing::debug!("registry: {}", config.registry);
for (scope, url) in &config.scoped_registries {
tracing::debug!("scoped registry: {scope} -> {url}");
}
let policy = resolve_fetch_policy(cwd);
aube_registry::client::RegistryClient::from_config_with_policy(config, policy)
}
pub(crate) async fn run_pnpmfile_pre_resolution(
paths: &[std::path::PathBuf],
cwd: &std::path::Path,
existing: Option<&aube_lockfile::LockfileGraph>,
) -> miette::Result<()> {
if paths.is_empty() {
return Ok(());
}
let config = load_npm_config(cwd);
let mut registries = std::collections::BTreeMap::new();
registries.insert("default".to_string(), config.registry);
for (scope, url) in config.scoped_registries {
registries.insert(scope, url);
}
let store_dir = resolved_store_dir(cwd).or_else(|| {
aube_store::dirs::store_dir()
.and_then(|p| p.parent()?.parent().map(std::path::Path::to_path_buf))
});
let ctx = crate::pnpmfile::PreResolutionContext::from_existing(
cwd,
store_dir.as_deref(),
existing,
registries,
);
crate::pnpmfile::run_pre_resolution_chain(paths, cwd, &ctx)
.await
.wrap_err("pnpmfile preResolution hook failed")
}
pub(crate) fn build_resolver(
cwd: &std::path::Path,
manifest: &aube_manifest::PackageJson,
catalogs: CatalogMap,
) -> aube_resolver::Resolver {
let (ws_config, raw_workspace) = aube_manifest::workspace::load_both(cwd).unwrap_or_default();
let files = FileSources::load(cwd);
let env = aube_settings::values::process_env();
let ctx = files.ctx(&raw_workspace, env, &[]);
let target_lockfile_kind = Some(
aube_lockfile::detect_existing_lockfile_kind(cwd)
.unwrap_or(aube_lockfile::LockfileKind::Aube),
);
install::configure_resolver(
aube_resolver::Resolver::new(std::sync::Arc::new(make_client(cwd))),
cwd,
manifest,
install::ResolverConfigInputs {
settings_ctx: &ctx,
workspace_config: &ws_config,
workspace_catalogs: &catalogs,
minimum_release_age_override: None,
target_lockfile_kind,
cache_full_packuments: false,
},
None,
)
}
pub(crate) fn resolve_fetch_policy(cwd: &std::path::Path) -> aube_registry::config::FetchPolicy {
let files = FileSources::load(cwd);
let workspace_yaml = aube_manifest::workspace::load_both(cwd)
.map(|(_, raw)| raw)
.unwrap_or_default();
let env = aube_settings::values::process_env();
let ctx = files.ctx(&workspace_yaml, env, fetch_cli_overrides());
aube_registry::config::FetchPolicy::from_ctx(&ctx)
}
pub(crate) fn resolved_cache_dir(cwd: &std::path::Path) -> std::path::PathBuf {
let platform_default =
|| aube_store::dirs::cache_dir().unwrap_or_else(|| std::env::temp_dir().join("aube"));
let npmrc = aube_registry::config::load_npmrc_entries(cwd);
let has_explicit = npmrc
.iter()
.any(|(k, _)| k == "cacheDir" || k == "cache-dir");
if !has_explicit {
return platform_default();
}
with_settings_ctx(cwd, |ctx| {
let raw = aube_settings::resolved::cache_dir(ctx);
expand_setting_path(&raw, cwd).unwrap_or_else(platform_default)
})
}
pub(crate) fn resolve_virtual_store_dir_max_length(ctx: &aube_settings::ResolveCtx<'_>) -> usize {
aube_settings::resolved::virtual_store_dir_max_length(ctx)
.map(|v| v as usize)
.unwrap_or(aube_lockfile::dep_path_filename::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH)
}
pub(crate) fn resolve_virtual_store_dir_max_length_for_cwd(cwd: &std::path::Path) -> usize {
with_settings_ctx(cwd, resolve_virtual_store_dir_max_length)
}
pub(crate) fn resolve_modules_dir_name_for_cwd(cwd: &std::path::Path) -> String {
with_settings_ctx(cwd, aube_settings::resolved::modules_dir)
}
pub(crate) fn project_modules_dir(cwd: &std::path::Path) -> std::path::PathBuf {
cwd.join(resolve_modules_dir_name_for_cwd(cwd))
}
pub(crate) fn resolve_virtual_store_dir(
ctx: &aube_settings::ResolveCtx<'_>,
project_dir: &std::path::Path,
) -> std::path::PathBuf {
let default_from_modules_dir = || {
let modules_dir = aube_settings::resolved::modules_dir(ctx);
project_dir.join(modules_dir).join(".aube")
};
let has_explicit_npmrc = [
ctx.project_aube_config,
ctx.project_npmrc,
ctx.user_aube_config,
ctx.user_npmrc,
]
.iter()
.any(|entries| {
entries
.iter()
.any(|(k, _)| k == "virtualStoreDir" || k == "virtual-store-dir")
});
let has_explicit_yaml = ctx.workspace_yaml.contains_key("virtualStoreDir");
let has_explicit_env = ctx.env.iter().any(|(k, _)| {
k == "npm_config_virtual_store_dir"
|| k == "NPM_CONFIG_VIRTUAL_STORE_DIR"
|| k == "AUBE_VIRTUAL_STORE_DIR"
});
if !(has_explicit_npmrc || has_explicit_yaml || has_explicit_env) {
return default_from_modules_dir();
}
let raw = aube_settings::resolved::virtual_store_dir(ctx);
expand_setting_path(&raw, project_dir).unwrap_or_else(default_from_modules_dir)
}
pub(crate) fn resolve_virtual_store_dir_for_cwd(cwd: &std::path::Path) -> std::path::PathBuf {
with_settings_ctx(cwd, |ctx| resolve_virtual_store_dir(ctx, cwd))
}
#[cfg(test)]
mod resolve_virtual_store_dir_tests {
use super::resolve_virtual_store_dir;
use aube_settings::ResolveCtx;
use std::collections::BTreeMap;
use std::path::PathBuf;
fn ctx_with_env<'a>(
env: &'a [(String, String)],
ws: &'a BTreeMap<String, yaml_serde::Value>,
) -> ResolveCtx<'a> {
ResolveCtx {
project_aube_config: &[],
project_npmrc: &[],
user_aube_config: &[],
user_npmrc: &[],
workspace_yaml: ws,
env,
cli: &[],
}
}
#[test]
fn default_when_no_explicit_override() {
let env = vec![];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/node_modules/.aube"),
);
}
#[test]
fn aube_env_var_relocates_virtual_store() {
let env = vec![("AUBE_VIRTUAL_STORE_DIR".into(), ".aube".into())];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/.aube"),
);
}
#[test]
fn npm_config_env_var_relocates_virtual_store() {
let env = vec![("npm_config_virtual_store_dir".into(), ".vstore".into())];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/.vstore"),
);
}
}
pub(crate) fn format_virtual_store_display_prefix(
aube_dir: &std::path::Path,
ref_dir: &std::path::Path,
) -> String {
if let Some(rel) = pathdiff::diff_paths(aube_dir, ref_dir)
&& !rel.as_os_str().is_empty()
&& !rel
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return format!("./{}/", rel.display());
}
format!("{}/", aube_dir.display())
}
pub(crate) fn packument_cache_dir() -> std::path::PathBuf {
let cwd = crate::dirs::cwd().unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
resolved_cache_dir(&cwd).join("packuments-v1")
}
pub(crate) fn max_satisfying_version(
packument: &aube_registry::Packument,
range_str: &str,
) -> Option<String> {
let range = node_semver::Range::parse(range_str).ok()?;
let mut best: Option<(&str, node_semver::Version)> = None;
for ver_str in packument.versions.keys() {
let Ok(v) = node_semver::Version::parse(ver_str) else {
continue;
};
if !v.satisfies(&range) {
continue;
}
if best.as_ref().is_none_or(|(_, b)| v > *b) {
best = Some((ver_str.as_str(), v));
}
}
best.map(|(key, _)| key.to_string())
}
pub(crate) fn packument_full_cache_dir() -> std::path::PathBuf {
let cwd = crate::dirs::cwd().unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
resolved_cache_dir(&cwd).join("packuments-full-v1")
}
pub(crate) type CatalogMap =
std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>;
fn merge_catalog_source(
out: &mut CatalogMap,
default_cat: &std::collections::BTreeMap<String, String>,
named_cats: &CatalogMap,
) {
if !default_cat.is_empty() {
let entry = out.entry("default".to_string()).or_default();
for (k, v) in default_cat {
entry.insert(k.clone(), v.clone());
}
}
for (name, entries) in named_cats {
let bucket = out.entry(name.clone()).or_default();
for (k, v) in entries {
bucket.insert(k.clone(), v.clone());
}
}
}
fn merge_manifest_catalogs(out: &mut CatalogMap, manifest: &aube_manifest::PackageJson) {
if let Some(ws) = &manifest.workspaces {
merge_catalog_source(out, ws.catalog(), ws.catalogs());
}
merge_catalog_source(out, &manifest.pnpm_catalog(), &manifest.pnpm_catalogs());
}
pub(crate) fn discover_catalogs(project_root: &std::path::Path) -> miette::Result<CatalogMap> {
use miette::{Context, IntoDiagnostic};
let mut out = CatalogMap::new();
let project_manifest_path = project_root.join("package.json");
let project_manifest = aube_manifest::PackageJson::from_path(&project_manifest_path).ok();
if let Some(m) = &project_manifest {
merge_manifest_catalogs(&mut out, m);
}
let workspace_yaml_dir = crate::dirs::find_workspace_yaml_root(project_root);
let workspace_root_dir = crate::dirs::find_workspace_root(project_root);
if let Some(dir) = &workspace_root_dir
&& dir != project_root
&& let Ok(m) = aube_manifest::PackageJson::from_path(&dir.join("package.json"))
{
merge_manifest_catalogs(&mut out, &m);
}
let yaml_dir = workspace_yaml_dir.as_deref().unwrap_or(project_root);
let (ws_config, _raw) = aube_manifest::workspace::load_both(yaml_dir)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
merge_catalog_source(&mut out, &ws_config.catalog, &ws_config.catalogs);
out.retain(|_, v| !v.is_empty());
Ok(out)
}
pub(crate) fn load_workspace_catalogs(cwd: &std::path::Path) -> miette::Result<CatalogMap> {
discover_catalogs(cwd)
}
pub(crate) fn load_manifest(manifest_path: &Path) -> miette::Result<aube_manifest::PackageJson> {
aube_manifest::PackageJson::from_path(manifest_path)
.map_err(miette::Report::new)
.wrap_err("failed to read package.json")
}
pub(crate) fn load_manifest_or_default(root: &Path) -> miette::Result<aube_manifest::PackageJson> {
let path = root.join("package.json");
if path.is_file() {
load_manifest(&path)
} else {
Ok(aube_manifest::PackageJson::default())
}
}
pub(crate) fn write_manifest_json<T: serde::Serialize>(
path: &Path,
value: &T,
) -> miette::Result<()> {
let json = serde_json::to_string_pretty(value)
.into_diagnostic()
.wrap_err("failed to serialize package.json")?;
write_manifest_atomic(path, format!("{json}\n").as_bytes())
.wrap_err("failed to write package.json")
}
pub(crate) fn update_manifest_json_object<F>(path: &Path, update: F) -> miette::Result<()>
where
F: FnOnce(&mut serde_json::Map<String, serde_json::Value>) -> miette::Result<()>,
{
let content = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err("failed to read package.json")?;
let mut json: serde_json::Value = serde_json::from_str(&content)
.into_diagnostic()
.wrap_err("failed to parse package.json")?;
let serde_json::Value::Object(obj) = &mut json else {
return Err(miette!("package.json must contain a JSON object"));
};
update(obj)?;
let json = serde_json::to_string_pretty(&json)
.into_diagnostic()
.wrap_err("failed to serialize package.json")?;
write_manifest_atomic(path, format!("{json}\n").as_bytes())
}
pub(crate) fn write_manifest_dep_sections(
path: &Path,
manifest: &aube_manifest::PackageJson,
) -> miette::Result<()> {
update_manifest_json_object(path, |obj| {
sync_manifest_dep_sections(obj, manifest);
Ok(())
})
}
pub(crate) fn sync_manifest_dep_sections(
obj: &mut serde_json::Map<String, serde_json::Value>,
manifest: &aube_manifest::PackageJson,
) {
sync_dep_section(obj, "dependencies", &manifest.dependencies);
sync_dep_section(obj, "devDependencies", &manifest.dev_dependencies);
sync_dep_section(obj, "peerDependencies", &manifest.peer_dependencies);
sync_dep_section(obj, "optionalDependencies", &manifest.optional_dependencies);
}
fn sync_dep_section(
obj: &mut serde_json::Map<String, serde_json::Value>,
key: &str,
deps: &std::collections::BTreeMap<String, String>,
) {
if deps.is_empty() {
obj.remove(key);
return;
}
let section = deps
.iter()
.map(|(name, spec)| (name.clone(), serde_json::Value::String(spec.clone())))
.collect();
obj.insert(key.to_string(), serde_json::Value::Object(section));
}
pub(crate) fn write_manifest_atomic(path: &Path, body: &[u8]) -> miette::Result<()> {
aube_util::fs_atomic::atomic_write(path, body)
.into_diagnostic()
.wrap_err_with(|| format!("failed to write {}", path.display()))
}
pub(crate) fn load_graph(
project_dir: &Path,
manifest: &aube_manifest::PackageJson,
missing_hint: &str,
) -> miette::Result<aube_lockfile::LockfileGraph> {
match aube_lockfile::parse_lockfile(project_dir, manifest) {
Ok(g) => Ok(g),
Err(aube_lockfile::Error::NotFound(_)) => Err(miette!("{missing_hint}")),
Err(e) => Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
}
}
pub(crate) fn collect_dep_closure(
graph: &aube_lockfile::LockfileGraph,
filter: DepFilter,
no_optional: bool,
) -> std::collections::BTreeMap<String, &aube_lockfile::LockedPackage> {
let mut out: std::collections::BTreeMap<String, &aube_lockfile::LockedPackage> =
std::collections::BTreeMap::new();
let mut stack: Vec<String> = graph
.root_deps()
.iter()
.filter(|d| filter.keeps(d.dep_type))
.filter(|d| !(no_optional && matches!(d.dep_type, aube_lockfile::DepType::Optional)))
.map(|d| d.dep_path.clone())
.collect();
while let Some(dep_path) = stack.pop() {
if out.contains_key(&dep_path) {
continue;
}
let Some(pkg) = graph.get_package(&dep_path) else {
continue;
};
out.insert(dep_path.clone(), pkg);
for (name, version) in &pkg.dependencies {
stack.push(format!("{name}@{version}"));
}
}
out
}
pub(crate) fn finish_filtered_workspace(
cwd: &Path,
result: miette::Result<()>,
) -> miette::Result<()> {
let restore =
retarget_cwd(cwd).wrap_err_with(|| format!("failed to restore cwd to {}", cwd.display()));
match result {
Ok(()) => restore,
Err(err) => {
let _ = restore;
Err(err)
}
}
}
pub(crate) fn write_and_log_lockfile(
cwd: &Path,
graph: &aube_lockfile::LockfileGraph,
manifest: &aube_manifest::PackageJson,
) -> miette::Result<std::path::PathBuf> {
let written_path = aube_lockfile::write_lockfile_preserving_existing(cwd, graph, manifest)
.into_diagnostic()
.wrap_err("failed to write lockfile")?;
eprintln!(
"Wrote {}",
written_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| written_path.display().to_string())
);
Ok(written_path)
}
pub(crate) fn find_workspace_root(start: &std::path::Path) -> miette::Result<std::path::PathBuf> {
crate::dirs::find_workspace_root(start).ok_or_else(|| {
miette!(
"no workspace root (aube-workspace.yaml, pnpm-workspace.yaml, or package.json with a `workspaces` field) found above {}",
start.display()
)
})
}
pub(crate) fn select_workspace_packages(
cwd: &std::path::Path,
filter: &aube_workspace::selector::EffectiveFilter,
command: &str,
) -> miette::Result<(
std::path::PathBuf,
Vec<aube_workspace::selector::SelectedPackage>,
)> {
let root = crate::dirs::find_workspace_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let workspace_pkgs = aube_workspace::find_workspace_packages(&root)
.map_err(|e| miette!("failed to discover workspace packages: {e}"))?;
if workspace_pkgs.is_empty() {
return Err(miette!(
"aube {command}: --filter requires a workspace root (aube-workspace.yaml, pnpm-workspace.yaml, or package.json with a `workspaces` field) at or above {}",
cwd.display()
));
}
let matched =
aube_workspace::selector::select_workspace_packages(&root, &workspace_pkgs, filter)
.map_err(|e| miette!("invalid --filter selector: {e}"))?;
if matched.is_empty() {
return Err(miette!(
"aube {command}: filter {filter:?} did not match any workspace package"
));
}
Ok((root, matched))
}
pub(crate) fn resolve_version(packument: &serde_json::Value, spec: Option<&str>) -> Option<String> {
let dist_tags = packument.get("dist-tags").and_then(|v| v.as_object());
let versions = packument.get("versions").and_then(|v| v.as_object())?;
let spec = match spec {
None | Some("") => {
return dist_tags?
.get("latest")
.and_then(|v| v.as_str())
.map(String::from);
}
Some(s) => s,
};
if let Some(tag) = dist_tags.and_then(|t| t.get(spec)).and_then(|v| v.as_str()) {
return Some(tag.to_string());
}
if versions.contains_key(spec) {
return Some(spec.to_string());
}
let range: node_semver::Range = spec.parse().ok()?;
versions
.keys()
.filter_map(|v| {
v.parse::<node_semver::Version>()
.ok()
.filter(|parsed| parsed.satisfies(&range))
.map(|parsed| (v.clone(), parsed))
})
.max_by(|a, b| a.1.cmp(&b.1))
.map(|(raw, _)| raw)
}
pub(crate) fn split_name_spec(input: &str) -> (&str, Option<&str>) {
aube_util::pkg::split_name_spec(input)
}
pub(crate) fn encode_package_name(name: &str) -> String {
if let Some(rest) = name.strip_prefix('@')
&& let Some((scope, pkg)) = rest.split_once('/')
{
return format!("@{scope}%2F{pkg}");
}
name.to_string()
}
#[cfg(test)]
mod encode_package_name_tests {
use super::encode_package_name;
#[test]
fn scoped_name_encodes_slash() {
assert_eq!(encode_package_name("@scope/pkg"), "@scope%2Fpkg");
}
#[test]
fn plain_name_passthrough() {
assert_eq!(encode_package_name("lodash"), "lodash");
}
#[test]
fn malformed_scoped_name_passthrough() {
assert_eq!(encode_package_name("@scope"), "@scope");
}
}
#[cfg(test)]
mod split_name_spec_tests {
use super::split_name_spec;
#[test]
fn plain_name() {
assert_eq!(split_name_spec("lodash"), ("lodash", None));
}
#[test]
fn name_with_version() {
assert_eq!(
split_name_spec("lodash@4.17.21"),
("lodash", Some("4.17.21"))
);
}
#[test]
fn name_with_range() {
assert_eq!(split_name_spec("lodash@^4"), ("lodash", Some("^4")));
}
#[test]
fn name_with_tag() {
assert_eq!(split_name_spec("react@next"), ("react", Some("next")));
}
#[test]
fn scoped_no_version() {
assert_eq!(split_name_spec("@babel/core"), ("@babel/core", None));
}
#[test]
fn scoped_with_version() {
assert_eq!(
split_name_spec("@babel/core@7.0.0"),
("@babel/core", Some("7.0.0"))
);
}
}
pub(crate) async fn ensure_installed(no_install: bool) -> miette::Result<()> {
if no_install {
return Ok(());
}
if skip_auto_install_on_package_manager_mismatch() {
return Ok(());
}
if std::env::var_os("npm_lifecycle_event").is_some() {
return Ok(());
}
let initial_cwd = crate::dirs::cwd()?;
let cwd = crate::dirs::find_workspace_root(&initial_cwd)
.or_else(|| crate::dirs::find_project_root(&initial_cwd))
.unwrap_or(initial_cwd);
let (skip_auto_install, optimistic_repeat) = with_settings_ctx(&cwd, |ctx| {
(
aube_settings::resolved::aube_no_auto_install(ctx),
aube_settings::resolved::optimistic_repeat_install(ctx),
)
});
if skip_auto_install {
return Ok(());
}
let g = global_frozen_override();
let needs = if optimistic_repeat {
crate::state::check_needs_install(&cwd)
} else {
Some("optimisticRepeatInstall=false".to_string())
};
let verify_mode = resolve_verify_deps_before_run(&cwd)?;
let Some(reason) = needs.or_else(|| g.map(|o| format!("global {} flag", o.cli_flag()))) else {
return Ok(());
};
match verify_mode {
VerifyDepsBeforeRun::Skip => return Ok(()),
VerifyDepsBeforeRun::Warn => {
eprintln!("Dependencies need install before run: {reason}");
return Ok(());
}
VerifyDepsBeforeRun::Error => {
return Err(miette!(
"dependencies need install before run: {reason}\nRun `aube install`, or set verifyDepsBeforeRun=install to let aube do it automatically."
));
}
VerifyDepsBeforeRun::Install => {}
}
eprintln!("Auto-installing: {reason}");
let mode = chained_frozen_mode(install::FrozenMode::Prefer);
let mut opts = install::InstallOptions::with_mode(mode);
opts.strict_no_lockfile = matches!(g, Some(install::FrozenOverride::Frozen));
install::run(opts).await?;
Ok(())
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum VerifyDepsBeforeRun {
Install,
Warn,
Error,
Skip,
}
fn resolve_verify_deps_before_run(cwd: &std::path::Path) -> miette::Result<VerifyDepsBeforeRun> {
let files = FileSources::load(cwd);
let empty_ws = std::collections::BTreeMap::new();
let env = aube_settings::values::process_env();
let ctx = files.ctx(&empty_ws, env, &[]);
let raw = aube_settings::resolved::verify_deps_before_run(&ctx);
Ok(match raw.trim().to_ascii_lowercase().as_str() {
"false" | "0" => VerifyDepsBeforeRun::Skip,
"warn" => VerifyDepsBeforeRun::Warn,
"error" => VerifyDepsBeforeRun::Error,
"prompt" | "install" => VerifyDepsBeforeRun::Install,
_ => VerifyDepsBeforeRun::Install,
})
}
pub(crate) fn remove_existing(path: &std::path::Path) -> miette::Result<()> {
if path.symlink_metadata().is_err() {
return Ok(());
}
if path.is_dir() && !path.is_symlink() {
std::fs::remove_dir_all(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove {}", path.display()))?;
} else {
std::fs::remove_file(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove {}", path.display()))?;
}
Ok(())
}
pub(crate) fn workspace_importer_path(
workspace_root: &std::path::Path,
dir: &std::path::Path,
) -> miette::Result<String> {
let rel = pathdiff::diff_paths(dir, workspace_root).ok_or_else(|| {
miette!(
"could not compute path of workspace package {} relative to {}",
dir.display(),
workspace_root.display()
)
})?;
if rel.as_os_str().is_empty() {
Ok(".".to_string())
} else {
Ok(rel.to_string_lossy().replace('\\', "/"))
}
}
pub(crate) fn symlink_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
aube_linker::create_dir_link(src, dst)
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum DepFilter {
All,
ProdOnly,
DevOnly,
}
impl DepFilter {
pub(crate) fn from_flags(prod: bool, dev: bool) -> Self {
match (prod, dev) {
(true, _) => Self::ProdOnly,
(_, true) => Self::DevOnly,
_ => Self::All,
}
}
pub(crate) fn keeps(self, dep_type: aube_lockfile::DepType) -> bool {
use aube_lockfile::DepType;
matches!(
(self, dep_type),
(Self::All, _)
| (Self::ProdOnly, DepType::Production | DepType::Optional)
| (Self::DevOnly, DepType::Dev)
)
}
}
#[cfg(test)]
mod dep_filter_tests {
use super::*;
use aube_lockfile::DepType;
#[test]
fn all_keeps_everything() {
let f = DepFilter::from_flags(false, false);
assert!(f.keeps(DepType::Production));
assert!(f.keeps(DepType::Dev));
assert!(f.keeps(DepType::Optional));
}
#[test]
fn prod_keeps_production_and_optional() {
let f = DepFilter::from_flags(true, false);
assert!(f.keeps(DepType::Production));
assert!(f.keeps(DepType::Optional));
assert!(!f.keeps(DepType::Dev));
}
#[test]
fn dev_keeps_only_dev() {
let f = DepFilter::from_flags(false, true);
assert!(!f.keeps(DepType::Production));
assert!(!f.keeps(DepType::Optional));
assert!(f.keeps(DepType::Dev));
}
#[test]
fn prod_wins_over_dev_when_both_set() {
let f = DepFilter::from_flags(true, true);
assert!(f.keeps(DepType::Production));
assert!(!f.keeps(DepType::Dev));
}
#[test]
fn package_manager_mismatch_skip_auto_install_defaults_off() {
assert!(!skip_auto_install_on_package_manager_mismatch());
}
}
#[cfg(test)]
mod manifest_write_tests {
use super::*;
#[test]
fn write_manifest_dep_sections_preserves_existing_top_level_order() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("package.json");
std::fs::write(
&path,
r#"{
"name": "example",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "echo test"
},
"devDependencies": {
"typescript": "^6.0.3"
}
}
"#,
)
.unwrap();
let mut manifest = aube_manifest::PackageJson::from_path(&path).unwrap();
manifest
.dev_dependencies
.insert("tstyche".to_string(), "^7.1.0".to_string());
write_manifest_dep_sections(&path, &manifest).unwrap();
let written = std::fs::read_to_string(&path).unwrap();
assert_eq!(
root_key_order(&written),
["name", "version", "license", "scripts", "devDependencies"]
);
assert!(written.contains(r#""tstyche": "^7.1.0""#));
}
#[test]
fn write_manifest_dep_sections_removes_empty_sections_without_reordering() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("package.json");
std::fs::write(
&path,
r#"{
"name": "example",
"devDependencies": {
"typescript": "^6.0.3"
},
"license": "MIT"
}
"#,
)
.unwrap();
let mut manifest = aube_manifest::PackageJson::from_path(&path).unwrap();
manifest.dev_dependencies.remove("typescript");
write_manifest_dep_sections(&path, &manifest).unwrap();
let written = std::fs::read_to_string(&path).unwrap();
assert_eq!(root_key_order(&written), ["name", "license"]);
assert!(!written.contains("devDependencies"));
}
fn root_key_order(raw: &str) -> Vec<String> {
let serde_json::Value::Object(obj) = serde_json::from_str(raw).unwrap() else {
panic!("expected object");
};
obj.keys().cloned().collect()
}
}