provider.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Package provider computes the runtime environment handed to the claude
  2. // subprocess: the union of all provider env keys (for cleanup), the resolved
  3. // env for the selected provider (dereferencing env:VAR indirections against
  4. // a snapshot of the parent environment), and the final child env slice.
  5. //
  6. // All functions here are pure — they never touch os.Environ or os.Setenv.
  7. // The caller is responsible for capturing the parent-env snapshot before
  8. // invoking any of these helpers; see cli.useCmd for orchestration.
  9. package provider
  10. import (
  11. "fmt"
  12. "sort"
  13. "strings"
  14. "github.com/kotoyuuko/cc-switch-cli/internal/config"
  15. )
  16. // UnionEnvKeys returns the sorted, deduplicated union of env key names across
  17. // every provider in the config. This is the "cleanup set" subtracted from the
  18. // inherited environment before the selected provider's env is injected.
  19. func UnionEnvKeys(providers map[string]config.Provider) []string {
  20. set := map[string]struct{}{}
  21. for _, p := range providers {
  22. for k := range p.Env {
  23. set[k] = struct{}{}
  24. }
  25. }
  26. out := make([]string, 0, len(set))
  27. for k := range set {
  28. out = append(out, k)
  29. }
  30. sort.Strings(out)
  31. return out
  32. }
  33. // EnvRefError is returned when a provider's env value references an env var
  34. // that is not present in the parent snapshot. The CLI prints its message and
  35. // exits non-zero WITHOUT launching claude.
  36. type EnvRefError struct {
  37. // Key is the env var the provider wants to set (e.g. ANTHROPIC_API_KEY).
  38. Key string
  39. // Var is the parent-env var that was referenced (e.g. MY_ANTHROPIC_KEY).
  40. Var string
  41. }
  42. func (e *EnvRefError) Error() string {
  43. return fmt.Sprintf("%s references env var %s which is not set", e.Key, e.Var)
  44. }
  45. // ResolveEnvRefs materialises a provider's env map by expanding any value of
  46. // the form `env:VAR_NAME` against parentSnapshot.
  47. //
  48. // Resolution happens exactly once: if the looked-up value itself matches the
  49. // env:VAR syntax, it is still treated as a literal string (no chained lookup).
  50. // If a reference cannot be satisfied, an *EnvRefError is returned and the map
  51. // is partially-built state; callers MUST NOT proceed to launch claude.
  52. //
  53. // parentSnapshot should be captured from os.Environ() BEFORE any cleanup, so
  54. // that a reference `env:X` can find `X` even when `X` is also in the
  55. // cleanup-union for some other provider.
  56. func ResolveEnvRefs(selected config.Provider, parentSnapshot map[string]string) (map[string]string, error) {
  57. out := make(map[string]string, len(selected.Env))
  58. for k, raw := range selected.Env {
  59. if varName, ok := config.IsEnvRef(raw); ok {
  60. val, present := parentSnapshot[varName]
  61. if !present {
  62. return nil, &EnvRefError{Key: k, Var: varName}
  63. }
  64. out[k] = val
  65. continue
  66. }
  67. out[k] = raw
  68. }
  69. return out, nil
  70. }
  71. // BuildChildEnv produces the KEY=VALUE slice to hand to exec.Cmd.Env.
  72. //
  73. // Contract:
  74. // 1. Start from `parent` (typically os.Environ()).
  75. // 2. Drop every entry whose key is in `union`.
  76. // 3. Append every entry from `resolved` (resolved values win on collisions
  77. // since map entries come after the filtered parent).
  78. //
  79. // No shell expansion is performed on values at any stage.
  80. func BuildChildEnv(parent []string, union []string, resolved map[string]string) []string {
  81. unionSet := make(map[string]struct{}, len(union))
  82. for _, k := range union {
  83. unionSet[k] = struct{}{}
  84. }
  85. // Also consider resolved keys as "to-drop" so that parent entries with the
  86. // same key don't linger before the provider's value. (If a key is both in
  87. // union and in resolved, it's dropped then added; same for resolved-only
  88. // keys.)
  89. for k := range resolved {
  90. unionSet[k] = struct{}{}
  91. }
  92. out := make([]string, 0, len(parent)+len(resolved))
  93. for _, kv := range parent {
  94. eq := strings.IndexByte(kv, '=')
  95. if eq < 0 {
  96. // malformed entry; keep it to stay faithful to inheritance
  97. out = append(out, kv)
  98. continue
  99. }
  100. k := kv[:eq]
  101. if _, drop := unionSet[k]; drop {
  102. continue
  103. }
  104. out = append(out, kv)
  105. }
  106. // Append provider-supplied entries in deterministic order for stable trace logs.
  107. keys := make([]string, 0, len(resolved))
  108. for k := range resolved {
  109. keys = append(keys, k)
  110. }
  111. sort.Strings(keys)
  112. for _, k := range keys {
  113. out = append(out, k+"="+resolved[k])
  114. }
  115. return out
  116. }
  117. // SnapshotEnv captures a parent env slice (KEY=VALUE) into a map for cheap
  118. // lookup during ref resolution. Later duplicates (same key) win, matching
  119. // standard shell semantics.
  120. func SnapshotEnv(env []string) map[string]string {
  121. m := make(map[string]string, len(env))
  122. for _, kv := range env {
  123. eq := strings.IndexByte(kv, '=')
  124. if eq < 0 {
  125. continue
  126. }
  127. m[kv[:eq]] = kv[eq+1:]
  128. }
  129. return m
  130. }