| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- package cli
- import (
- "bufio"
- "fmt"
- "os"
- "sort"
- "strconv"
- "strings"
- "github.com/spf13/cobra"
- "github.com/kotoyuuko/cc-switch-cli/internal/provider"
- "github.com/kotoyuuko/cc-switch-cli/internal/runner"
- )
- const pickerMaxRetries = 3
- func newUseCmd(app *appState) *cobra.Command {
- return &cobra.Command{
- Use: "use [name]",
- Short: "Launch claude with a provider's env",
- Args: cobra.MaximumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- return runUse(cmd, app, args)
- },
- }
- }
- func runUse(cmd *cobra.Command, app *appState, args []string) error {
- if len(app.cfg.Providers) == 0 {
- return fmt.Errorf("no providers configured; run `cc-switch add <name> ...` first")
- }
- name, err := selectProvider(app, args)
- if err != nil {
- return err
- }
- p, ok := app.cfg.Providers[name]
- if !ok {
- return fmt.Errorf("provider %q does not exist", name)
- }
- app.tracef("selected provider: %s", name)
- // Resolve claude path BEFORE we start doing any env work, so a missing
- // binary fails loud and early.
- claudePath, err := runner.ResolveClaudePath(app.cfg)
- if err != nil {
- return err
- }
- app.tracef("claude path: %s", claudePath)
- // 1. Snapshot parent env (before any cleanup or mutation).
- snapshot := provider.SnapshotEnv(os.Environ())
- // 2. Resolve env:VAR references against that snapshot.
- resolved, err := provider.ResolveEnvRefs(p, snapshot)
- if err != nil {
- return err
- }
- // 3. Compute union of all providers' env keys (cleanup set).
- union := provider.UnionEnvKeys(app.cfg.Providers)
- app.tracef("union env keys: %s", strings.Join(union, ","))
- // 4. Build the child env.
- childEnv := provider.BuildChildEnv(os.Environ(), union, resolved)
- // For trace, print the final injected keys (never values).
- if app.verbose {
- keys := make([]string, 0, len(resolved))
- for k := range resolved {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- app.tracef("injecting keys: %s", strings.Join(keys, ","))
- }
- // Pass through any extra argv after `use <name>` if support were ever
- // added; for now Args caps us at 1 arg, so no extra args flow through.
- // (cobra stores them in positional args; we already consumed args[0].)
- extraArgs := []string{}
- if len(args) > 1 {
- extraArgs = args[1:]
- }
- code, runErr := runner.Run(claudePath, childEnv, extraArgs)
- // Record whatever claude's exit code was so the CLI propagates it.
- exit := code
- app.requestedExit = &exit
- app.tracef("cleanup complete; claude exit code=%d", code)
- if runErr != nil {
- // A launch/wait failure. Surface it as the returned error while
- // still honoring the requested exit code.
- return runErr
- }
- return nil
- }
- // selectProvider decides which provider to run. Rules (from cli spec):
- // - if args has one name, validate and return it.
- // - if tty: interactive menu, up to pickerMaxRetries attempts.
- // - if non-tty: fall back to default_provider, else error.
- func selectProvider(app *appState, args []string) (string, error) {
- if len(args) >= 1 {
- name := args[0]
- if _, ok := app.cfg.Providers[name]; !ok {
- return "", fmt.Errorf("provider %q does not exist (see `cc-switch list`)", name)
- }
- return name, nil
- }
- if !isTTY(app.stdin) {
- if app.cfg.DefaultProvider != "" {
- return app.cfg.DefaultProvider, nil
- }
- return "", fmt.Errorf(
- "non-interactive invocation requires a provider name or a configured default " +
- "(set one with `cc-switch config set default <name>`)")
- }
- return promptProvider(app)
- }
- // promptProvider renders the numbered list and reads user input. Empty input
- // selects the default (if set). The user may type a number or a name.
- func promptProvider(app *appState) (string, error) {
- names := app.cfg.SortedProviderNames()
- reader := bufio.NewReader(app.stdin)
- for attempt := 0; attempt < pickerMaxRetries; attempt++ {
- fmt.Fprintln(app.stdout, "Select provider:")
- for i, n := range names {
- marker := " "
- if n == app.cfg.DefaultProvider {
- marker = "* "
- }
- fmt.Fprintf(app.stdout, "%s%d) %s\n", marker, i+1, n)
- }
- if app.cfg.DefaultProvider != "" {
- fmt.Fprintf(app.stdout, "> [%s] ", app.cfg.DefaultProvider)
- } else {
- fmt.Fprint(app.stdout, "> ")
- }
- line, err := reader.ReadString('\n')
- if err != nil && line == "" {
- return "", err
- }
- input := strings.TrimSpace(line)
- if input == "" {
- if app.cfg.DefaultProvider == "" {
- fmt.Fprintln(app.stderr, "no default provider configured; enter a number or name")
- continue
- }
- return app.cfg.DefaultProvider, nil
- }
- if idx, err := strconv.Atoi(input); err == nil {
- if idx >= 1 && idx <= len(names) {
- return names[idx-1], nil
- }
- fmt.Fprintf(app.stderr, "invalid selection %d\n", idx)
- continue
- }
- if _, ok := app.cfg.Providers[input]; ok {
- return input, nil
- }
- fmt.Fprintf(app.stderr, "unknown provider %q\n", input)
- }
- return "", fmt.Errorf("gave up after %d invalid selections", pickerMaxRetries)
- }
|