add.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package cli
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "strings"
  7. "github.com/spf13/cobra"
  8. "github.com/kotoyuuko/cc-switch-cli/internal/config"
  9. "github.com/kotoyuuko/cc-switch-cli/internal/templates"
  10. )
  11. func newAddCmd(app *appState) *cobra.Command {
  12. var (
  13. envFlags []string
  14. description string
  15. fromTemplate string
  16. nonInteractive bool
  17. )
  18. cmd := &cobra.Command{
  19. Use: "add <name>",
  20. Short: "Add a new provider",
  21. Args: cobra.ExactArgs(1),
  22. RunE: func(cmd *cobra.Command, args []string) error {
  23. return runAdd(app, args[0], envFlags, description, fromTemplate, nonInteractive)
  24. },
  25. }
  26. cmd.Flags().StringArrayVar(&envFlags, "env", nil,
  27. "KEY=VALUE env entry (repeatable)")
  28. cmd.Flags().StringVar(&description, "description", "",
  29. "optional human-readable description")
  30. cmd.Flags().StringVar(&fromTemplate, "from-template", "",
  31. "use a built-in template as the env skeleton")
  32. cmd.Flags().BoolVar(&nonInteractive, "non-interactive", false,
  33. "never prompt; require values via --env / template defaults")
  34. return cmd
  35. }
  36. func runAdd(app *appState, name string, envFlags []string, description, fromTemplate string, nonInteractive bool) error {
  37. if err := config.ValidateProviderName(name); err != nil {
  38. return err
  39. }
  40. if _, exists := app.cfg.Providers[name]; exists {
  41. return fmt.Errorf("provider %q already exists (use `edit` or pick a different name)", name)
  42. }
  43. // Parse --env flags into a map up front; flags override any template seed.
  44. explicitEnv, err := parseEnvFlags(envFlags)
  45. if err != nil {
  46. return err
  47. }
  48. var env map[string]string
  49. tty := isTTY(app.stdin)
  50. switch {
  51. case fromTemplate != "":
  52. tpl, ok := templates.Get(fromTemplate)
  53. if !ok {
  54. return fmt.Errorf("unknown template %q; run `cc-switch templates list` to see available",
  55. fromTemplate)
  56. }
  57. env = make(map[string]string, len(tpl.Env))
  58. if nonInteractive || !tty {
  59. // Fill with defaults for keys the user didn't override.
  60. for _, e := range tpl.Env {
  61. if v, set := explicitEnv[e.Name]; set {
  62. env[e.Name] = v
  63. continue
  64. }
  65. env[e.Name] = e.Default
  66. }
  67. // Flags might also introduce keys not in the template.
  68. for k, v := range explicitEnv {
  69. if _, already := env[k]; !already {
  70. env[k] = v
  71. }
  72. }
  73. } else {
  74. env, err = runTemplateWizard(app, tpl, explicitEnv)
  75. if err != nil {
  76. return err
  77. }
  78. }
  79. case len(explicitEnv) > 0:
  80. env = explicitEnv
  81. default:
  82. // No template, no --env.
  83. if !tty || nonInteractive {
  84. return fmt.Errorf(
  85. "non-interactive invocation requires --env KEY=VALUE or --from-template <tpl>")
  86. }
  87. env, err = runFreeWizard(app)
  88. if err != nil {
  89. return err
  90. }
  91. }
  92. // Reject empty values — the spec forbids landing with a placeholder.
  93. for k, v := range env {
  94. if v == "" {
  95. return fmt.Errorf("env key %q has no value (after template+flags)", k)
  96. }
  97. }
  98. if len(env) == 0 {
  99. return fmt.Errorf("provider %q: env must contain at least one key", name)
  100. }
  101. p := config.Provider{Description: description, Env: env}
  102. if err := app.cfg.AddProvider(name, p); err != nil {
  103. return err
  104. }
  105. if err := app.save(); err != nil {
  106. return err
  107. }
  108. fmt.Fprintf(app.stdout, "added provider %q (%d env keys)\n", name, len(env))
  109. return nil
  110. }
  111. // parseEnvFlags converts --env KEY=VALUE (repeatable) to a map. KEY must be
  112. // non-empty; VALUE may contain '=' freely.
  113. func parseEnvFlags(flags []string) (map[string]string, error) {
  114. out := make(map[string]string, len(flags))
  115. for _, kv := range flags {
  116. eq := strings.IndexByte(kv, '=')
  117. if eq <= 0 {
  118. return nil, fmt.Errorf("--env %q: expected KEY=VALUE", kv)
  119. }
  120. k := kv[:eq]
  121. v := kv[eq+1:]
  122. out[k] = v
  123. }
  124. return out, nil
  125. }
  126. // runTemplateWizard walks the user through each template key. Values from
  127. // explicitEnv are treated as "already provided" and skipped. Empty input
  128. // accepts the template default if present; otherwise the user must supply
  129. // a non-empty value.
  130. func runTemplateWizard(app *appState, tpl templates.Template, explicit map[string]string) (map[string]string, error) {
  131. env := make(map[string]string, len(tpl.Env))
  132. reader := bufio.NewReader(app.stdin)
  133. fmt.Fprintf(app.stdout, "using template %q: %s\n", tpl.Name, tpl.Description)
  134. for _, e := range tpl.Env {
  135. if v, set := explicit[e.Name]; set {
  136. env[e.Name] = v
  137. fmt.Fprintf(app.stdout, " %s: (from --env)\n", e.Name)
  138. continue
  139. }
  140. prompt := fmt.Sprintf(" %s", e.Name)
  141. if e.Hint != "" {
  142. prompt += fmt.Sprintf(" (%s)", e.Hint)
  143. }
  144. if e.Default != "" {
  145. prompt += fmt.Sprintf(" [%s]", e.Default)
  146. }
  147. prompt += ": "
  148. fmt.Fprint(app.stdout, prompt)
  149. line, err := readLine(reader)
  150. if err != nil {
  151. return nil, err
  152. }
  153. line = strings.TrimSpace(line)
  154. if line == "" {
  155. if e.Default == "" {
  156. return nil, fmt.Errorf("key %q requires a value", e.Name)
  157. }
  158. env[e.Name] = e.Default
  159. } else {
  160. env[e.Name] = line
  161. }
  162. }
  163. // Allow --env to introduce extra keys the template didn't list.
  164. for k, v := range explicit {
  165. if _, already := env[k]; !already {
  166. env[k] = v
  167. }
  168. }
  169. return env, nil
  170. }
  171. // runFreeWizard keeps prompting for key/value pairs until the user gives an
  172. // empty key.
  173. func runFreeWizard(app *appState) (map[string]string, error) {
  174. env := map[string]string{}
  175. reader := bufio.NewReader(app.stdin)
  176. fmt.Fprintln(app.stdout, "Enter env keys one at a time. Blank key to finish.")
  177. for {
  178. fmt.Fprint(app.stdout, " KEY: ")
  179. k, err := readLine(reader)
  180. if err != nil {
  181. return nil, err
  182. }
  183. k = strings.TrimSpace(k)
  184. if k == "" {
  185. break
  186. }
  187. fmt.Fprintf(app.stdout, " %s value: ", k)
  188. v, err := readLine(reader)
  189. if err != nil {
  190. return nil, err
  191. }
  192. env[k] = strings.TrimRight(v, "\r\n")
  193. }
  194. if len(env) == 0 {
  195. return nil, fmt.Errorf("no env keys supplied")
  196. }
  197. return env, nil
  198. }
  199. // readLine reads one line (without trailing newline). On EOF with no data it
  200. // returns io.EOF so callers can distinguish end-of-stream from empty input.
  201. func readLine(r *bufio.Reader) (string, error) {
  202. line, err := r.ReadString('\n')
  203. if len(line) == 0 && err != nil {
  204. return "", err
  205. }
  206. return strings.TrimRight(line, "\n"), nil
  207. }
  208. // silence "unused import" on io if we drop readLine later.
  209. var _ io.Reader