diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go new file mode 100644 index 00000000..248020c1 --- /dev/null +++ b/cmd/configure/configure.go @@ -0,0 +1,149 @@ +package configure + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + + "github.com/alecthomas/kong" + "github.com/buildkite/cli/v3/internal/cli" + "github.com/buildkite/cli/v3/internal/io" + "github.com/buildkite/cli/v3/pkg/cmd/factory" + "github.com/buildkite/cli/v3/pkg/cmd/validation" +) + +type ConfigureCmd struct { + Org string `help:"Organization slug" optional:""` + Token string `help:"API token" optional:""` + Force bool `help:"Force setting a new token" optional:""` + Default ConfigureDefaultCmd `cmd:"" optional:"" help:"Configure Buildkite API token" hidden:"" default:"1"` + Add ConfigureAddCmd `cmd:"" optional:"" help:"Add configuration for a new organization"` +} + +type ConfigureDefaultCmd struct { +} + +type ConfigureAddCmd struct { +} + +func (c *ConfigureAddCmd) Help() string { + return ` +Examples: + # Prompt configuration to add for a new organization + $ bk configure add + + # Add configure Buildkite API token + $ bk configure add --org my-org --token my-token +` +} + +func (c *ConfigureCmd) Help() string { + return ` +Examples: + # Configure Buildkite API token + $ bk configure --org my-org --token my-token + + # Force setting a new token + $ bk configure --force --org my-org --token my-token +` +} + +func (c *ConfigureCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { + f, err := factory.New() + + if err != nil { + return err + } + + f.SkipConfirm = globals.SkipConfirmation() + f.NoInput = globals.DisableInput() + f.Quiet = globals.IsQuiet() + + if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { + return err + } + + if kongCtx.Command() == "configure default" { + if !c.Force && f.Config.APIToken() != "" { + return errors.New("API token already configured. You must use --force") + } + + } + + // If flags are provided, use them directly + if c.Org != "" && c.Token != "" { + return ConfigureWithCredentials(f, c.Org, c.Token) + } + + return ConfigureRun(f, c.Org) +} + +func ConfigureWithCredentials(f *factory.Factory, org, token string) error { + if err := f.Config.SelectOrganization(org, f.GitRepository != nil); err != nil { + return err + } + return f.Config.SetTokenForOrg(org, token) +} + +func ConfigureRun(f *factory.Factory, org string) error { + // Check if we're in a Git repository + if f.GitRepository == nil { + return errors.New("not in a Git repository - bk should be configured at the root of a Git repository") + } + + if org == "" { + // Get organization slug + inputOrg, err := promptForInput("Organization slug: ", false) + + if err != nil { + return err + } + if inputOrg == "" { + return errors.New("organization slug cannot be empty") + } + org = inputOrg + } + // Check if token already exists for this organization + existingToken := getTokenForOrg(f, org) + if existingToken != "" { + fmt.Printf("Using existing API token for organization: %s\n", org) + return f.Config.SelectOrganization(org, f.GitRepository != nil) + } + + // Get API token with password input (no echo) + token, err := promptForInput("API Token: ", true) + if err != nil { + return err + } + if token == "" { + return errors.New("API token cannot be empty") + } + + fmt.Println("API token set for organization:", org) + return ConfigureWithCredentials(f, org, token) +} + +// getTokenForOrg retrieves the token for a specific organization from the user config +func getTokenForOrg(f *factory.Factory, org string) string { + return f.Config.GetTokenForOrg(org) +} + +// promptForInput handles terminal input with optional password masking +func promptForInput(prompt string, isPassword bool) (string, error) { + fmt.Print(prompt) + + if isPassword { + return io.ReadPassword() + } else { + // Use standard input for regular text + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + return "", err + } + // Trim whitespace and newlines + return strings.TrimSpace(input), nil + } +} diff --git a/pkg/cmd/configure/add/add_test.go b/cmd/configure/configure_test.go similarity index 98% rename from pkg/cmd/configure/add/add_test.go rename to cmd/configure/configure_test.go index 26e37eb1..1d65a10e 100644 --- a/pkg/cmd/configure/add/add_test.go +++ b/cmd/configure/configure_test.go @@ -1,4 +1,4 @@ -package add +package configure import ( "testing" @@ -136,7 +136,7 @@ func TestConfigureRequiresGitRepository(t *testing.T) { // Create a factory with nil GitRepository (simulating not being in a git repo) f := &factory.Factory{Config: conf, GitRepository: nil} - err := ConfigureRun(f) + err := ConfigureRun(f, "test-org") if err == nil { t.Error("expected error when not in a git repository, got nil") diff --git a/main.go b/main.go index 9d5414c3..24f6e188 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/buildkite/cli/v3/cmd/artifacts" "github.com/buildkite/cli/v3/cmd/build" "github.com/buildkite/cli/v3/cmd/cluster" + "github.com/buildkite/cli/v3/cmd/configure" bkInit "github.com/buildkite/cli/v3/cmd/init" "github.com/buildkite/cli/v3/cmd/job" "github.com/buildkite/cli/v3/cmd/organization" @@ -109,56 +110,10 @@ type ( api.ApiCmd `cmd:"" help:"Interact with the Buildkite API"` } ConfigureCmd struct { - Args []string `arg:"" optional:"" passthrough:"all"` + configure.ConfigureCmd `cmd:"" help:"Configure Buildkite API token"` } ) -// Delegation methods, we should delete when native Kong implementations ready -func (c *ConfigureCmd) Run(cli *CLI) error { return cli.delegateToCobraSystem("configure", c.Args) } - -// delegateToCobraSystem delegates execution to the legacy Cobra command system. -// This is a temporary bridge during the Kong migration that ensures backwards compatibility -// by reconstructing global flags that Kong has already parsed. -func (cli *CLI) delegateToCobraSystem(command string, args []string) error { - // Preserve and restore original args for safety - originalArgs := os.Args - defer func() { os.Args = originalArgs }() - - // Reconstruct command args with global flags for Cobra compatibility - reconstructedArgs := cli.buildCobraArgs(command, args) - os.Args = reconstructedArgs - - if code := runCobraSystem(); code != 0 { - os.Exit(code) - } - return nil -} - -// buildCobraArgs constructs the argument slice for Cobra, including global flags. -// Kong parses and consumes global flags before delegation, so we need to reconstruct -// them to maintain backwards compatibility with Cobra commands. -func (cli *CLI) buildCobraArgs(command string, passthroughArgs []string) []string { - args := []string{os.Args[0], command} - - if cli.Yes { - args = append(args, "--yes") - } - if cli.NoInput { - args = append(args, "--no-input") - } - if cli.Quiet { - args = append(args, "--quiet") - } - // TODO: Add verbose flag reconstruction when implemented - // if cli.Verbose { - // args = append(args, "--verbose") - // } - - args = append(args, passthroughArgs...) - - return args -} - func runCobraSystem() int { f, err := factory.New() if err != nil { @@ -315,6 +270,9 @@ func isHelpRequest() bool { if len(os.Args) >= 2 && os.Args[1] == "user" { return false } + if len(os.Args) >= 2 && os.Args[1] == "configure" { + return false + } if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { return true diff --git a/pkg/cmd/configure/add/add.go b/pkg/cmd/configure/add/add.go deleted file mode 100644 index a84000b9..00000000 --- a/pkg/cmd/configure/add/add.go +++ /dev/null @@ -1,100 +0,0 @@ -package add - -import ( - "bufio" - "errors" - "fmt" - "os" - "strings" - "syscall" - - "github.com/buildkite/cli/v3/pkg/cmd/factory" - "github.com/spf13/cobra" - "golang.org/x/term" -) - -func NewCmdAdd(f *factory.Factory) *cobra.Command { - cmd := &cobra.Command{ - Use: "add", - Args: cobra.NoArgs, - Short: "Add config for new organization", - Long: "Add configuration for a new organization.", - RunE: func(cmd *cobra.Command, args []string) error { - return ConfigureRun(f) - }, - } - - return cmd -} - -func ConfigureWithCredentials(f *factory.Factory, org, token string) error { - if err := f.Config.SelectOrganization(org, f.GitRepository != nil); err != nil { - return err - } - - return f.Config.SetTokenForOrg(org, token) -} - -func ConfigureRun(f *factory.Factory) error { - // Check if we're in a Git repository - if f.GitRepository == nil { - return errors.New("not in a Git repository - bk should be configured at the root of a Git repository") - } - - // Get organization slug - org, err := promptForInput("Organization slug: ", false) - if err != nil { - return err - } - if org == "" { - return errors.New("organization slug cannot be empty") - } - - // Check if token already exists for this organization - existingToken := getTokenForOrg(f, org) - if existingToken != "" { - fmt.Printf("Using existing API token for organization: %s\n", org) - return f.Config.SelectOrganization(org, f.GitRepository != nil) - } - - // Get API token with password input (no echo) - token, err := promptForInput("API Token: ", true) - if err != nil { - return err - } - if token == "" { - return errors.New("API token cannot be empty") - } - - fmt.Println("API token set for organization:", org) - return ConfigureWithCredentials(f, org, token) -} - -// getTokenForOrg retrieves the token for a specific organization from the user config -func getTokenForOrg(f *factory.Factory, org string) string { - return f.Config.GetTokenForOrg(org) -} - -// promptForInput handles terminal input with optional password masking -func promptForInput(prompt string, isPassword bool) (string, error) { - fmt.Print(prompt) - - if isPassword { - // Use term.ReadPassword for secure password input - passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() // Add a newline after password input - if err != nil { - return "", err - } - return string(passwordBytes), nil - } else { - // Use standard input for regular text - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - return "", err - } - // Trim whitespace and newlines - return strings.TrimSpace(input), nil - } -} diff --git a/pkg/cmd/configure/configure.go b/pkg/cmd/configure/configure.go deleted file mode 100644 index 6d1c1564..00000000 --- a/pkg/cmd/configure/configure.go +++ /dev/null @@ -1,46 +0,0 @@ -package configure - -import ( - "errors" - - addCmd "github.com/buildkite/cli/v3/pkg/cmd/configure/add" - "github.com/buildkite/cli/v3/pkg/cmd/factory" - "github.com/spf13/cobra" -) - -func NewCmdConfigure(f *factory.Factory) *cobra.Command { - var ( - force bool - org string - token string - ) - - cmd := &cobra.Command{ - Use: "configure", - Aliases: []string{"config"}, - Args: cobra.NoArgs, - Short: "Configure Buildkite API token", - RunE: func(cmd *cobra.Command, args []string) error { - // if the token already exists and --force is not used - if !force && f.Config.APIToken() != "" { - return errors.New("API token already configured. You must use --force") - } - - // If flags are provided, use them directly - if org != "" && token != "" { - return addCmd.ConfigureWithCredentials(f, org, token) - } - - // Otherwise fall back to interactive mode - return addCmd.ConfigureRun(f) - }, - } - - cmd.Flags().BoolVar(&force, "force", false, "Force setting a new token") - cmd.Flags().StringVar(&org, "org", "", "Organization slug") - cmd.Flags().StringVar(&token, "token", "", "API token") - - cmd.AddCommand(addCmd.NewCmdAdd(f)) - - return cmd -} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index e7921da1..defe0d54 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - configureCmd "github.com/buildkite/cli/v3/pkg/cmd/configure" "github.com/buildkite/cli/v3/pkg/cmd/factory" initCmd "github.com/buildkite/cli/v3/pkg/cmd/init" promptCmd "github.com/buildkite/cli/v3/pkg/cmd/prompt" @@ -41,7 +40,6 @@ func NewCmdRoot(f *factory.Factory) (*cobra.Command, error) { }, } - cmd.AddCommand(configureCmd.NewCmdConfigure(f)) cmd.AddCommand(initCmd.NewCmdInit(f)) cmd.AddCommand(promptCmd.NewCmdPrompt(f))