use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
const DEFAULT_STATE_DIR: &str = "node_modules";
const STATE_FILE_NAME: &str = ".aube-state";
fn resolve_paths(project_dir: &Path) -> (PathBuf, PathBuf) {
crate::commands::with_settings_ctx(project_dir, |ctx| {
let modules_dir = project_dir.join(aube_settings::resolved::modules_dir(ctx));
let raw_state = aube_settings::resolved::state_dir(ctx);
let state_dir = if raw_state == DEFAULT_STATE_DIR {
modules_dir.clone()
} else {
crate::commands::expand_setting_path(&raw_state, project_dir)
.unwrap_or_else(|| modules_dir.clone())
};
let state_file = state_dir.join(STATE_FILE_NAME);
(modules_dir, state_file)
})
}
fn state_file(project_dir: &Path) -> PathBuf {
resolve_paths(project_dir).1
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InstallState {
pub lockfile_hash: String,
pub package_json_hashes: BTreeMap<String, String>,
pub aube_version: String,
#[serde(default, rename = "prod")]
pub section_filtered: bool,
}
pub fn check_needs_install(project_dir: &Path) -> Option<String> {
let (modules_dir, state_path) = resolve_paths(project_dir);
let state = match read_state(&state_path) {
Some(s) => s,
None => return Some("install state not found".into()),
};
if !modules_dir.exists() {
let name = modules_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("node_modules");
return Some(format!("{name} is missing"));
}
let (lockfile_name, lockfile_path) = active_lockfile(project_dir);
if let Some(path) = lockfile_path {
let current_hash = hash_file(&path);
if current_hash != state.lockfile_hash {
return Some(format!("{lockfile_name} has changed"));
}
} else {
return Some("no lockfile found".into());
}
let pkg_path = project_dir.join("package.json");
if pkg_path.exists() {
let current_hash = hash_file(&pkg_path);
let stored_hash = state.package_json_hashes.get(".");
if stored_hash != Some(¤t_hash) {
return Some("package.json has changed".into());
}
}
if state.section_filtered {
return Some(
"previous install omitted dependency sections; auto-installing full graph".into(),
);
}
None
}
pub fn write_state(project_dir: &Path, section_filtered: bool) -> Result<(), std::io::Error> {
let mut package_json_hashes = BTreeMap::new();
let pkg_path = project_dir.join("package.json");
if pkg_path.exists() {
package_json_hashes.insert(".".to_string(), hash_file(&pkg_path));
}
let lockfile_hash = match active_lockfile(project_dir).1 {
Some(path) => hash_file(&path),
None => String::new(),
};
let state = InstallState {
lockfile_hash,
package_json_hashes,
aube_version: env!("CARGO_PKG_VERSION").to_string(),
section_filtered,
};
let state_path = state_file(project_dir);
if let Some(parent) = state_path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&state)?;
std::fs::write(state_path, json)?;
Ok(())
}
pub fn remove_state(project_dir: &Path) -> Result<(), std::io::Error> {
match std::fs::remove_file(state_file(project_dir)) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}
fn active_lockfile(project_dir: &Path) -> (String, Option<PathBuf>) {
let preferred = aube_lockfile::aube_lock_filename(project_dir);
let preferred_path = project_dir.join(&preferred);
if preferred_path.exists() {
return (preferred, Some(preferred_path));
}
if preferred != "aube-lock.yaml" {
let base = project_dir.join("aube-lock.yaml");
if base.exists() {
return ("aube-lock.yaml".to_string(), Some(base));
}
}
let pnpm_preferred = preferred.replacen("aube-lock.", "pnpm-lock.", 1);
if pnpm_preferred != preferred {
let pnpm_branch = project_dir.join(&pnpm_preferred);
if pnpm_branch.exists() {
return (pnpm_preferred, Some(pnpm_branch));
}
}
let pnpm_base = project_dir.join("pnpm-lock.yaml");
if pnpm_base.exists() {
return ("pnpm-lock.yaml".to_string(), Some(pnpm_base));
}
for name in [
"bun.lock",
"yarn.lock",
"npm-shrinkwrap.json",
"package-lock.json",
] {
let path = project_dir.join(name);
if path.exists() {
return (name.to_string(), Some(path));
}
}
(preferred, None)
}
fn read_state(path: &PathBuf) -> Option<InstallState> {
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn hash_file(path: &Path) -> String {
let content = std::fs::read(path).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(&content);
let hash = hasher.finalize();
format!("sha256:{}", hex::encode(hash))
}