#![forbid(unsafe_code)]
use std::path::{Path, PathBuf};
use vanta_core::{StoreKey, VtaResult};
use vanta_store::{hash_bytes, link_best, Store};
#[derive(Debug, Clone, PartialEq)]
pub struct EnvTool {
pub tool: String,
pub key: StoreKey,
pub bins: Vec<String>,
}
pub fn env_id(tools: &[EnvTool]) -> String {
let mut members: Vec<String> = tools
.iter()
.map(|t| format!("{}={}", t.tool, t.key.as_str()))
.collect();
members.sort();
let blob = members.join("\n");
hash_bytes(blob.as_bytes())
.strip_prefix("blake3-")
.unwrap_or("env")
.chars()
.take(32)
.collect()
}
pub fn compose(store: &Store, home: &Path, tools: &[EnvTool]) -> VtaResult<PathBuf> {
let id = env_id(tools);
let bin_dir = home.join("envs").join(&id).join("bin");
std::fs::create_dir_all(&bin_dir).map_err(|e| {
vanta_core::VtaError::new(
vanta_core::Area::Env,
1,
format!("creating env dir {}: {e}", bin_dir.display()),
)
})?;
for tool in tools {
let entry = store.entry_path(&tool.key);
for bin in &tool.bins {
let src = entry.join(bin);
let dst = bin_dir.join(basename(bin));
link_best(&src, &dst)?;
}
}
Ok(bin_dir)
}
pub fn activate_hook(shell: &str) -> Option<String> {
let hook = match shell {
"bash" => BASH,
"zsh" => ZSH,
"fish" => FISH,
"pwsh" | "powershell" => PWSH,
_ => return None,
};
Some(hook.to_string())
}
fn basename(p: &str) -> String {
p.rsplit(['/', '\\']).next().unwrap_or(p).to_string()
}
const BASH: &str = r#"# vanta shell hook (bash)
case ":$PATH:" in *":$HOME/.vanta/bin:"*) ;; *) export PATH="$HOME/.vanta/bin:$PATH";; esac
"#;
const ZSH: &str = r#"# vanta shell hook (zsh)
case ":$PATH:" in *":$HOME/.vanta/bin:"*) ;; *) export PATH="$HOME/.vanta/bin:$PATH";; esac
"#;
const FISH: &str = r#"# vanta shell hook (fish)
if not contains "$HOME/.vanta/bin" $PATH
set -gx PATH "$HOME/.vanta/bin" $PATH
end
"#;
const PWSH: &str = r#"# vanta shell hook (PowerShell)
if ($env:PATH -notlike "*$HOME\.vanta\bin*") { $env:PATH = "$HOME\.vanta\bin;$env:PATH" }
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_id_is_order_independent_and_stable() {
let a = EnvTool {
tool: "node".into(),
key: StoreKey::new(format!("blake3-{}", "a".repeat(64))).unwrap(),
bins: vec![],
};
let b = EnvTool {
tool: "go".into(),
key: StoreKey::new(format!("blake3-{}", "b".repeat(64))).unwrap(),
bins: vec![],
};
let id1 = env_id(&[a.clone(), b.clone()]);
let id2 = env_id(&[b, a]);
assert_eq!(id1, id2);
assert!(!id1.is_empty());
}
#[test]
fn activate_known_and_unknown() {
assert!(activate_hook("zsh").unwrap().contains("vanta"));
assert!(activate_hook("bash").is_some());
assert!(activate_hook("tcsh").is_none());
}
#[test]
fn compose_links_bins() {
let home = std::env::temp_dir().join(format!("vanta-env-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&home);
let store = Store::open(&home).unwrap();
let staged = store.new_staging().unwrap();
std::fs::create_dir_all(staged.join("bin")).unwrap();
std::fs::write(staged.join("bin/node"), b"#!/bin/true").unwrap();
let key = store.publish_tree(&staged).unwrap();
let tools = vec![EnvTool {
tool: "node".into(),
key,
bins: vec!["bin/node".into()],
}];
let bin_dir = compose(&store, &home, &tools).unwrap();
assert!(bin_dir.join("node").exists());
let _ = std::fs::remove_dir_all(&home);
}
}