Skip to main content

agent_skill_installer/
env.rs

1// SPDX-License-Identifier: LGPL-3.0-or-later
2// Copyright (C) 2026 Fabian Schmieder
3
4//! The parts of the process environment the planner depends on.
5//!
6//! Passed in rather than read inside, so a test can describe a whole machine
7//! in a temporary directory and a program can plan for a home other than its
8//! own.
9
10use std::collections::BTreeMap;
11use std::ffi::OsString;
12use std::path::{Path, PathBuf};
13
14/// A home directory and the environment variables that relocate tools.
15#[derive(Clone, PartialEq, Eq)]
16pub struct Environment {
17    home: PathBuf,
18    vars: BTreeMap<String, OsString>,
19}
20
21/// Names only: the process environment holds tokens and passwords, and a
22/// program that logs its environment must not log those.
23impl std::fmt::Debug for Environment {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("Environment")
26            .field("home", &self.home)
27            .field("vars", &self.vars.keys().collect::<Vec<_>>())
28            .finish()
29    }
30}
31
32impl Environment {
33    /// An environment with the given home and no variables set.
34    #[must_use]
35    pub fn new(home: impl Into<PathBuf>) -> Self {
36        Self {
37            home: home.into(),
38            vars: BTreeMap::new(),
39        }
40    }
41
42    /// The same environment with one variable set.
43    #[must_use]
44    pub fn with_var(mut self, name: impl Into<String>, value: impl Into<OsString>) -> Self {
45        self.vars.insert(name.into(), value.into());
46        self
47    }
48
49    /// The environment of the running process.
50    ///
51    /// Returns `None` when no home directory can be found, because every
52    /// target lives below it and guessing one would write somewhere unknown.
53    #[must_use]
54    pub fn from_process() -> Option<Self> {
55        let home = std::env::var_os("HOME")
56            .or_else(|| std::env::var_os("USERPROFILE"))
57            .filter(|value| !value.is_empty())?;
58        let vars = std::env::vars_os()
59            .filter_map(|(name, value)| Some((name.into_string().ok()?, value)))
60            .collect();
61        Some(Self {
62            home: PathBuf::from(home),
63            vars,
64        })
65    }
66
67    /// The home directory.
68    #[must_use]
69    pub fn home(&self) -> &Path {
70        &self.home
71    }
72
73    /// A variable, when it is set to something other than the empty string.
74    ///
75    /// An empty value is treated as unset, as most tools do: `CLAUDE_CONFIG_DIR=`
76    /// does not move anything to the current directory.
77    #[must_use]
78    pub fn var(&self, name: &str) -> Option<&OsString> {
79        self.vars.get(name).filter(|value| !value.is_empty())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    /// The process environment holds secrets; Debug shows only names.
88    #[test]
89    fn debug_shows_variable_names_not_values() {
90        let env = Environment::new("/home/x").with_var("API_TOKEN", "s3cr3t-value");
91        let shown = format!("{env:?}");
92        assert!(shown.contains("API_TOKEN"), "{shown}");
93        assert!(!shown.contains("s3cr3t-value"), "{shown}");
94    }
95}