| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- // 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
- }
|