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 ...` 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 ` 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 `)") } 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) }