// Package provider computes the runtime environment handed to the claude // subprocess: the union of all provider env keys (for cleanup), the resolved // env for the selected provider (dereferencing env:VAR indirections against // a snapshot of the parent environment), and the final child env slice. // // All functions here are pure — they never touch os.Environ or os.Setenv. // The caller is responsible for capturing the parent-env snapshot before // invoking any of these helpers; see cli.useCmd for orchestration. package provider import ( "fmt" "sort" "strings" "github.com/kotoyuuko/cc-switch-cli/internal/config" ) // UnionEnvKeys returns the sorted, deduplicated union of env key names across // every provider in the config. This is the "cleanup set" subtracted from the // inherited environment before the selected provider's env is injected. func UnionEnvKeys(providers map[string]config.Provider) []string { set := map[string]struct{}{} for _, p := range providers { for k := range p.Env { set[k] = struct{}{} } } out := make([]string, 0, len(set)) for k := range set { out = append(out, k) } sort.Strings(out) return out } // EnvRefError is returned when a provider's env value references an env var // that is not present in the parent snapshot. The CLI prints its message and // exits non-zero WITHOUT launching claude. type EnvRefError struct { // Key is the env var the provider wants to set (e.g. ANTHROPIC_API_KEY). Key string // Var is the parent-env var that was referenced (e.g. MY_ANTHROPIC_KEY). Var string } func (e *EnvRefError) Error() string { return fmt.Sprintf("%s references env var %s which is not set", e.Key, e.Var) } // ResolveEnvRefs materialises a provider's env map by expanding any value of // the form `env:VAR_NAME` against parentSnapshot. // // Resolution happens exactly once: if the looked-up value itself matches the // env:VAR syntax, it is still treated as a literal string (no chained lookup). // If a reference cannot be satisfied, an *EnvRefError is returned and the map // is partially-built state; callers MUST NOT proceed to launch claude. // // parentSnapshot should be captured from os.Environ() BEFORE any cleanup, so // that a reference `env:X` can find `X` even when `X` is also in the // cleanup-union for some other provider. func ResolveEnvRefs(selected config.Provider, parentSnapshot map[string]string) (map[string]string, error) { out := make(map[string]string, len(selected.Env)) for k, raw := range selected.Env { if varName, ok := config.IsEnvRef(raw); ok { val, present := parentSnapshot[varName] if !present { return nil, &EnvRefError{Key: k, Var: varName} } out[k] = val continue } out[k] = raw } return out, nil } // BuildChildEnv produces the KEY=VALUE slice to hand to exec.Cmd.Env. // // Contract: // 1. Start from `parent` (typically os.Environ()). // 2. Drop every entry whose key is in `union`. // 3. Append every entry from `resolved` (resolved values win on collisions // since map entries come after the filtered parent). // // No shell expansion is performed on values at any stage. func BuildChildEnv(parent []string, union []string, resolved map[string]string) []string { unionSet := make(map[string]struct{}, len(union)) for _, k := range union { unionSet[k] = struct{}{} } // Also consider resolved keys as "to-drop" so that parent entries with the // same key don't linger before the provider's value. (If a key is both in // union and in resolved, it's dropped then added; same for resolved-only // keys.) for k := range resolved { unionSet[k] = struct{}{} } out := make([]string, 0, len(parent)+len(resolved)) for _, kv := range parent { eq := strings.IndexByte(kv, '=') if eq < 0 { // malformed entry; keep it to stay faithful to inheritance out = append(out, kv) continue } k := kv[:eq] if _, drop := unionSet[k]; drop { continue } out = append(out, kv) } // Append provider-supplied entries in deterministic order for stable trace logs. keys := make([]string, 0, len(resolved)) for k := range resolved { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { out = append(out, k+"="+resolved[k]) } return out } // SnapshotEnv captures a parent env slice (KEY=VALUE) into a map for cheap // lookup during ref resolution. Later duplicates (same key) win, matching // standard shell semantics. func SnapshotEnv(env []string) map[string]string { m := make(map[string]string, len(env)) for _, kv := range env { eq := strings.IndexByte(kv, '=') if eq < 0 { continue } m[kv[:eq]] = kv[eq+1:] } return m }