use.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package cli
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "github.com/spf13/cobra"
  10. "github.com/kotoyuuko/cc-switch-cli/internal/provider"
  11. "github.com/kotoyuuko/cc-switch-cli/internal/runner"
  12. )
  13. const pickerMaxRetries = 3
  14. func newUseCmd(app *appState) *cobra.Command {
  15. return &cobra.Command{
  16. Use: "use [name]",
  17. Short: "Launch claude with a provider's env",
  18. Args: cobra.MaximumNArgs(1),
  19. RunE: func(cmd *cobra.Command, args []string) error {
  20. return runUse(cmd, app, args)
  21. },
  22. }
  23. }
  24. func runUse(cmd *cobra.Command, app *appState, args []string) error {
  25. if len(app.cfg.Providers) == 0 {
  26. return fmt.Errorf("no providers configured; run `cc-switch add <name> ...` first")
  27. }
  28. name, err := selectProvider(app, args)
  29. if err != nil {
  30. return err
  31. }
  32. p, ok := app.cfg.Providers[name]
  33. if !ok {
  34. return fmt.Errorf("provider %q does not exist", name)
  35. }
  36. app.tracef("selected provider: %s", name)
  37. // Resolve claude path BEFORE we start doing any env work, so a missing
  38. // binary fails loud and early.
  39. claudePath, err := runner.ResolveClaudePath(app.cfg)
  40. if err != nil {
  41. return err
  42. }
  43. app.tracef("claude path: %s", claudePath)
  44. // 1. Snapshot parent env (before any cleanup or mutation).
  45. snapshot := provider.SnapshotEnv(os.Environ())
  46. // 2. Resolve env:VAR references against that snapshot.
  47. resolved, err := provider.ResolveEnvRefs(p, snapshot)
  48. if err != nil {
  49. return err
  50. }
  51. // 3. Compute union of all providers' env keys (cleanup set).
  52. union := provider.UnionEnvKeys(app.cfg.Providers)
  53. app.tracef("union env keys: %s", strings.Join(union, ","))
  54. // 4. Build the child env.
  55. childEnv := provider.BuildChildEnv(os.Environ(), union, resolved)
  56. // For trace, print the final injected keys (never values).
  57. if app.verbose {
  58. keys := make([]string, 0, len(resolved))
  59. for k := range resolved {
  60. keys = append(keys, k)
  61. }
  62. sort.Strings(keys)
  63. app.tracef("injecting keys: %s", strings.Join(keys, ","))
  64. }
  65. // Pass through any extra argv after `use <name>` if support were ever
  66. // added; for now Args caps us at 1 arg, so no extra args flow through.
  67. // (cobra stores them in positional args; we already consumed args[0].)
  68. extraArgs := []string{}
  69. if len(args) > 1 {
  70. extraArgs = args[1:]
  71. }
  72. code, runErr := runner.Run(claudePath, childEnv, extraArgs)
  73. // Record whatever claude's exit code was so the CLI propagates it.
  74. exit := code
  75. app.requestedExit = &exit
  76. app.tracef("cleanup complete; claude exit code=%d", code)
  77. if runErr != nil {
  78. // A launch/wait failure. Surface it as the returned error while
  79. // still honoring the requested exit code.
  80. return runErr
  81. }
  82. return nil
  83. }
  84. // selectProvider decides which provider to run. Rules (from cli spec):
  85. // - if args has one name, validate and return it.
  86. // - if tty: interactive menu, up to pickerMaxRetries attempts.
  87. // - if non-tty: fall back to default_provider, else error.
  88. func selectProvider(app *appState, args []string) (string, error) {
  89. if len(args) >= 1 {
  90. name := args[0]
  91. if _, ok := app.cfg.Providers[name]; !ok {
  92. return "", fmt.Errorf("provider %q does not exist (see `cc-switch list`)", name)
  93. }
  94. return name, nil
  95. }
  96. if !isTTY(app.stdin) {
  97. if app.cfg.DefaultProvider != "" {
  98. return app.cfg.DefaultProvider, nil
  99. }
  100. return "", fmt.Errorf(
  101. "non-interactive invocation requires a provider name or a configured default " +
  102. "(set one with `cc-switch config set default <name>`)")
  103. }
  104. return promptProvider(app)
  105. }
  106. // promptProvider renders the numbered list and reads user input. Empty input
  107. // selects the default (if set). The user may type a number or a name.
  108. func promptProvider(app *appState) (string, error) {
  109. names := app.cfg.SortedProviderNames()
  110. reader := bufio.NewReader(app.stdin)
  111. for attempt := 0; attempt < pickerMaxRetries; attempt++ {
  112. fmt.Fprintln(app.stdout, "Select provider:")
  113. for i, n := range names {
  114. marker := " "
  115. if n == app.cfg.DefaultProvider {
  116. marker = "* "
  117. }
  118. fmt.Fprintf(app.stdout, "%s%d) %s\n", marker, i+1, n)
  119. }
  120. if app.cfg.DefaultProvider != "" {
  121. fmt.Fprintf(app.stdout, "> [%s] ", app.cfg.DefaultProvider)
  122. } else {
  123. fmt.Fprint(app.stdout, "> ")
  124. }
  125. line, err := reader.ReadString('\n')
  126. if err != nil && line == "" {
  127. return "", err
  128. }
  129. input := strings.TrimSpace(line)
  130. if input == "" {
  131. if app.cfg.DefaultProvider == "" {
  132. fmt.Fprintln(app.stderr, "no default provider configured; enter a number or name")
  133. continue
  134. }
  135. return app.cfg.DefaultProvider, nil
  136. }
  137. if idx, err := strconv.Atoi(input); err == nil {
  138. if idx >= 1 && idx <= len(names) {
  139. return names[idx-1], nil
  140. }
  141. fmt.Fprintf(app.stderr, "invalid selection %d\n", idx)
  142. continue
  143. }
  144. if _, ok := app.cfg.Providers[input]; ok {
  145. return input, nil
  146. }
  147. fmt.Fprintf(app.stderr, "unknown provider %q\n", input)
  148. }
  149. return "", fmt.Errorf("gave up after %d invalid selections", pickerMaxRetries)
  150. }