package add
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
"gitcode.com/gitcode-cli/cli/api"
cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"
"gitcode.com/gitcode-cli/cli/pkg/iostreams"
)
type AddOptions struct {
IO *iostreams.IOStreams
HttpClient func() (*http.Client, error)
Title string
KeyFile string
JSON bool
}
func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command {
opts := &AddOptions{IO: f.IOStreams, HttpClient: f.HttpClient}
cmd := &cobra.Command{
Use: "add",
Short: "Add an SSH public key",
Long: heredoc.Doc("Add an SSH public key from an explicitly provided file."),
Example: heredoc.Doc("$ gc ssh-key add --title laptop --key ~/.ssh/id_ed25519.pub\n$ gc ssh-key add --title laptop --key key.pub --json"),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)
}
return addRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Title for the SSH key")
cmd.Flags().StringVarP(&opts.KeyFile, "key", "k", "", "Path to the SSH public key file")
cmdutil.AddJSONFlag(cmd, &opts.JSON)
_ = cmd.MarkFlagRequired("title")
_ = cmd.MarkFlagRequired("key")
return cmd
}
func addRun(opts *AddOptions) error {
title := strings.TrimSpace(opts.Title)
if title == "" {
return cmdutil.NewUsageError("--title cannot be empty")
}
keyText, err := cmdutil.ReadTextFile(opts.KeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH public key file: %w", err)
}
keyText = strings.TrimSpace(keyText)
if err := validatePublicKey(keyText); err != nil {
return err
}
client, err := cmdutil.AuthenticatedClientFromFactory(opts.HttpClient)
if err != nil {
return err
}
key, err := api.CreateSSHKey(client, &api.CreateSSHKeyOptions{Title: title, Key: keyText})
if err != nil {
return fmt.Errorf("failed to add SSH key: %w", err)
}
if opts.JSON {
return cmdutil.WriteJSON(opts.IO.Out, key)
}
fmt.Fprintf(opts.IO.Out, "Added SSH key %s (ID: %d).\n", cmdutil.SanitizeTerminalText(key.Title), key.ID)
return nil
}
func validatePublicKey(keyText string) error {
if keyText == "" {
return cmdutil.NewUsageError("SSH public key file is empty")
}
if strings.Contains(keyText, "PRIVATE KEY") {
return cmdutil.NewUsageError("--key must reference a public key, not a private key")
}
if strings.ContainsAny(keyText, "\r\n") {
return cmdutil.NewUsageError("SSH public key must contain exactly one line; expected a single OpenSSH public key")
}
if !publicKeyPattern.MatchString(keyText) {
return cmdutil.NewUsageError("SSH public key must be a single OpenSSH public key line")
}
return nil
}
var publicKeyPattern = regexp.MustCompile(`^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-[^\s]+|sk-(ssh-ed25519|ecdsa-sha2-[^\s]+)@openssh\.com)\s+[A-Za-z0-9+/]+={0,3}(\s+.*)?$`)