| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- 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 <name>",
- 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 <tpl>")
- }
- 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
|