// Package env manages the daemon-global environment for ollie. // It owns the set of known OLLIE_* and SUPERPOWERD_* variables, provides // defaults, and formats them for export to frontends via ollie/env. package env import ( "bufio" "fmt" "os" "path/filepath" "strings" "ollie/paths" ) // managed is the ordered list of env vars that ollie exposes to frontends. var managed = []string{ "OLLIE", "OLLIE_CFG_PATH", "OLLIE_TOOLS_PATH", "OLLIE_AGENTS_PATH", "OLLIE_SKILLS_PATH", "OLLIE_PROMPTS_PATH", "OLLIE_MEMORY_PATH", "OLLIE_TMP_PATH", "OLLIE_TRANSCRIPT_PATH", "OLLIE_ELEVATE_SOCKET", "SUPERPOWERD_SESSION_TOKEN", "SUPERPOWERD_SOCKET_DIR", } // EnsureDefaults loads ~/.config/ollie/env and sets default values for any // OLLIE_* vars not already present in the process environment. func EnsureDefaults() { home, _ := os.UserHomeDir() LoadFile(filepath.Join(home, ".config", "ollie", "env")) xdgRuntime := os.Getenv("XDG_RUNTIME_DIR") if xdgRuntime == "" { xdgRuntime = fmt.Sprintf("/run/user/%d", os.Getuid()) } defaults := map[string]string{ "OLLIE": filepath.Join(home, "mnt", "ollie"), "OLLIE_CFG_PATH": filepath.Join(home, ".config", "ollie"), "OLLIE_TOOLS_PATH": filepath.Join(home, ".config", "ollie", "tools"), "OLLIE_AGENTS_PATH": filepath.Join(home, ".config", "ollie", "agents"), "OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"), "OLLIE_PROMPTS_PATH": filepath.Join(home, ".config", "ollie", "prompts"), "OLLIE_MEMORY_PATH": filepath.Join(home, ".config", "ollie", "memory"), "OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"), "OLLIE_TRANSCRIPT_PATH": filepath.Join(home, ".config", "ollie", "transcript"), "OLLIE_ELEVATE_SOCKET": filepath.Join(xdgRuntime, "ollie", "elevate.sock"), } for k, v := range defaults { if os.Getenv(k) == "" { os.Setenv(k, v) //nolint:errcheck } } } // Set sets a variable in the process environment. func Set(k, v string) { os.Setenv(k, v) } //nolint:errcheck // LoadFile reads KEY=VALUE pairs from path and sets any key not already // present in the environment. Lines beginning with # and blank lines are // ignored. Missing file is silently ignored. func LoadFile(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, "=") if !ok { continue } k = strings.TrimSpace(k) if i := strings.Index(v, " #"); i >= 0 { v = v[:i] } v = strings.TrimSpace(v) if len(v) >= 2 && (v[0] == '"' && v[len(v)-1] == '"' || v[0] == '\'' && v[len(v)-1] == '\'') { v = v[1 : len(v)-1] } if k != "" && os.Getenv(k) == "" { os.Setenv(k, paths.ExpandHome(os.ExpandEnv(v))) //nolint:errcheck } } } // Get returns a variable from the process environment. func Get(k string) string { return os.Getenv(k) } // All returns the full process environment as a map. func All() map[string]string { pairs := os.Environ() m := make(map[string]string, len(pairs)) for _, pair := range pairs { k, v, _ := strings.Cut(pair, "=") m[k] = v } return m } // Format returns the managed environment variables as NAME=VALUE lines, // suitable for serving as ollie/env. func Format() []byte { var sb strings.Builder for _, k := range managed { if v := os.Getenv(k); v != "" { fmt.Fprintf(&sb, "%s=%s\n", k, v) } } return []byte(sb.String()) }