1#![forbid(unsafe_code)]
8
9use std::path::{Path, PathBuf};
10use vanta_core::{StoreKey, VtaResult};
11use vanta_store::{hash_bytes, link_best, Store};
12
13#[derive(Debug, Clone, PartialEq)]
16pub struct EnvTool {
17 pub tool: String,
18 pub key: StoreKey,
19 pub bins: Vec<String>,
20}
21
22pub fn env_id(tools: &[EnvTool]) -> String {
24 let mut members: Vec<String> = tools
25 .iter()
26 .map(|t| format!("{}={}", t.tool, t.key.as_str()))
27 .collect();
28 members.sort();
29 let blob = members.join("\n");
30 hash_bytes(blob.as_bytes())
32 .strip_prefix("blake3-")
33 .unwrap_or("env")
34 .chars()
35 .take(32)
36 .collect()
37}
38
39pub fn compose(store: &Store, home: &Path, tools: &[EnvTool]) -> VtaResult<PathBuf> {
43 let id = env_id(tools);
44 let bin_dir = home.join("envs").join(&id).join("bin");
45 std::fs::create_dir_all(&bin_dir).map_err(|e| {
46 vanta_core::VtaError::new(
47 vanta_core::Area::Env,
48 1,
49 format!("creating env dir {}: {e}", bin_dir.display()),
50 )
51 })?;
52 for tool in tools {
53 let entry = store.entry_path(&tool.key);
54 for bin in &tool.bins {
55 let src = entry.join(bin);
56 let dst = bin_dir.join(basename(bin));
57 link_best(&src, &dst)?;
58 }
59 }
60 Ok(bin_dir)
61}
62
63pub fn activate_hook(shell: &str) -> Option<String> {
66 let hook = match shell {
67 "bash" => BASH,
68 "zsh" => ZSH,
69 "fish" => FISH,
70 "pwsh" | "powershell" => PWSH,
71 _ => return None,
72 };
73 Some(hook.to_string())
74}
75
76fn basename(p: &str) -> String {
77 p.rsplit(['/', '\\']).next().unwrap_or(p).to_string()
78}
79
80const BASH: &str = r#"# vanta shell hook (bash)
84case ":$PATH:" in *":$HOME/.vanta/bin:"*) ;; *) export PATH="$HOME/.vanta/bin:$PATH";; esac
85"#;
86
87const ZSH: &str = r#"# vanta shell hook (zsh)
88case ":$PATH:" in *":$HOME/.vanta/bin:"*) ;; *) export PATH="$HOME/.vanta/bin:$PATH";; esac
89"#;
90
91const FISH: &str = r#"# vanta shell hook (fish)
92if not contains "$HOME/.vanta/bin" $PATH
93 set -gx PATH "$HOME/.vanta/bin" $PATH
94end
95"#;
96
97const PWSH: &str = r#"# vanta shell hook (PowerShell)
98if ($env:PATH -notlike "*$HOME\.vanta\bin*") { $env:PATH = "$HOME\.vanta\bin;$env:PATH" }
99"#;
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn env_id_is_order_independent_and_stable() {
107 let a = EnvTool {
108 tool: "node".into(),
109 key: StoreKey::new(format!("blake3-{}", "a".repeat(64))).unwrap(),
110 bins: vec![],
111 };
112 let b = EnvTool {
113 tool: "go".into(),
114 key: StoreKey::new(format!("blake3-{}", "b".repeat(64))).unwrap(),
115 bins: vec![],
116 };
117 let id1 = env_id(&[a.clone(), b.clone()]);
118 let id2 = env_id(&[b, a]);
119 assert_eq!(id1, id2);
120 assert!(!id1.is_empty());
121 }
122
123 #[test]
124 fn activate_known_and_unknown() {
125 assert!(activate_hook("zsh").unwrap().contains("vanta"));
126 assert!(activate_hook("bash").is_some());
127 assert!(activate_hook("tcsh").is_none());
128 }
129
130 #[test]
131 fn compose_links_bins() {
132 let home = std::env::temp_dir().join(format!("vanta-env-{}", std::process::id()));
133 let _ = std::fs::remove_dir_all(&home);
134 let store = Store::open(&home).unwrap();
135 let staged = store.new_staging().unwrap();
136 std::fs::create_dir_all(staged.join("bin")).unwrap();
137 std::fs::write(staged.join("bin/node"), b"#!/bin/true").unwrap();
138 let key = store.publish_tree(&staged).unwrap();
139
140 let tools = vec![EnvTool {
141 tool: "node".into(),
142 key,
143 bins: vec!["bin/node".into()],
144 }];
145 let bin_dir = compose(&store, &home, &tools).unwrap();
146 assert!(bin_dir.join("node").exists());
147 let _ = std::fs::remove_dir_all(&home);
148 }
149}