package cli import ( "bufio" "fmt" "io" "strings" "github.com/spf13/cobra" "github.com/kotoyuuko/cc-switch-cli/internal/config" "github.com/kotoyuuko/cc-switch-cli/internal/templates" ) func newAddCmd(app *appState) *cobra.Command { var ( envFlags []string description string fromTemplate string nonInteractive bool ) cmd := &cobra.Command{ Use: "add ", Short: "Add a new provider", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runAdd(app, args[0], envFlags, description, fromTemplate, nonInteractive) }, } cmd.Flags().StringArrayVar(&envFlags, "env", nil, "KEY=VALUE env entry (repeatable)") cmd.Flags().StringVar(&description, "description", "", "optional human-readable description") cmd.Flags().StringVar(&fromTemplate, "from-template", "", "use a built-in template as the env skeleton") cmd.Flags().BoolVar(&nonInteractive, "non-interactive", false, "never prompt; require values via --env / template defaults") return cmd } func runAdd(app *appState, name string, envFlags []string, description, fromTemplate string, nonInteractive bool) error { if err := config.ValidateProviderName(name); err != nil { return err } if _, exists := app.cfg.Providers[name]; exists { return fmt.Errorf("provider %q already exists (use `edit` or pick a different name)", name) } // Parse --env flags into a map up front; flags override any template seed. explicitEnv, err := parseEnvFlags(envFlags) if err != nil { return err } var env map[string]string tty := isTTY(app.stdin) switch { case fromTemplate != "": tpl, ok := templates.Get(fromTemplate) if !ok { return fmt.Errorf("unknown template %q; run `cc-switch templates list` to see available", fromTemplate) } env = make(map[string]string, len(tpl.Env)) if nonInteractive || !tty { // Fill with defaults for keys the user didn't override. for _, e := range tpl.Env { if v, set := explicitEnv[e.Name]; set { env[e.Name] = v continue } env[e.Name] = e.Default } // Flags might also introduce keys not in the template. for k, v := range explicitEnv { if _, already := env[k]; !already { env[k] = v } } } else { env, err = runTemplateWizard(app, tpl, explicitEnv) if err != nil { return err } } case len(explicitEnv) > 0: env = explicitEnv default: // No template, no --env. if !tty || nonInteractive { return fmt.Errorf( "non-interactive invocation requires --env KEY=VALUE or --from-template ") } env, err = runFreeWizard(app) if err != nil { return err } } // Reject empty values — the spec forbids landing with a placeholder. for k, v := range env { if v == "" { return fmt.Errorf("env key %q has no value (after template+flags)", k) } } if len(env) == 0 { return fmt.Errorf("provider %q: env must contain at least one key", name) } p := config.Provider{Description: description, Env: env} if err := app.cfg.AddProvider(name, p); err != nil { return err } if err := app.save(); err != nil { return err } fmt.Fprintf(app.stdout, "added provider %q (%d env keys)\n", name, len(env)) return nil } // parseEnvFlags converts --env KEY=VALUE (repeatable) to a map. KEY must be // non-empty; VALUE may contain '=' freely. func parseEnvFlags(flags []string) (map[string]string, error) { out := make(map[string]string, len(flags)) for _, kv := range flags { eq := strings.IndexByte(kv, '=') if eq <= 0 { return nil, fmt.Errorf("--env %q: expected KEY=VALUE", kv) } k := kv[:eq] v := kv[eq+1:] out[k] = v } return out, nil } // runTemplateWizard walks the user through each template key. Values from // explicitEnv are treated as "already provided" and skipped. Empty input // accepts the template default if present; otherwise the user must supply // a non-empty value. func runTemplateWizard(app *appState, tpl templates.Template, explicit map[string]string) (map[string]string, error) { env := make(map[string]string, len(tpl.Env)) reader := bufio.NewReader(app.stdin) fmt.Fprintf(app.stdout, "using template %q: %s\n", tpl.Name, tpl.Description) for _, e := range tpl.Env { if v, set := explicit[e.Name]; set { env[e.Name] = v fmt.Fprintf(app.stdout, " %s: (from --env)\n", e.Name) continue } prompt := fmt.Sprintf(" %s", e.Name) if e.Hint != "" { prompt += fmt.Sprintf(" (%s)", e.Hint) } if e.Default != "" { prompt += fmt.Sprintf(" [%s]", e.Default) } prompt += ": " fmt.Fprint(app.stdout, prompt) line, err := readLine(reader) if err != nil { return nil, err } line = strings.TrimSpace(line) if line == "" { if e.Default == "" { return nil, fmt.Errorf("key %q requires a value", e.Name) } env[e.Name] = e.Default } else { env[e.Name] = line } } // Allow --env to introduce extra keys the template didn't list. for k, v := range explicit { if _, already := env[k]; !already { env[k] = v } } return env, nil } // runFreeWizard keeps prompting for key/value pairs until the user gives an // empty key. func runFreeWizard(app *appState) (map[string]string, error) { env := map[string]string{} reader := bufio.NewReader(app.stdin) fmt.Fprintln(app.stdout, "Enter env keys one at a time. Blank key to finish.") for { fmt.Fprint(app.stdout, " KEY: ") k, err := readLine(reader) if err != nil { return nil, err } k = strings.TrimSpace(k) if k == "" { break } fmt.Fprintf(app.stdout, " %s value: ", k) v, err := readLine(reader) if err != nil { return nil, err } env[k] = strings.TrimRight(v, "\r\n") } if len(env) == 0 { return nil, fmt.Errorf("no env keys supplied") } return env, nil } // readLine reads one line (without trailing newline). On EOF with no data it // returns io.EOF so callers can distinguish end-of-stream from empty input. func readLine(r *bufio.Reader) (string, error) { line, err := r.ReadString('\n') if len(line) == 0 && err != nil { return "", err } return strings.TrimRight(line, "\n"), nil } // silence "unused import" on io if we drop readLine later. var _ io.Reader