Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ require (
github.com/buildkite/go-buildkite/v4 v4.10.0
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/huh/spinner v0.0.0-20240608175402-5b41f0b45136
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/x/exp/teatest v0.0.0-20231215171016-7ba2b450712d
Expand All @@ -34,21 +33,18 @@ require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
Expand Down
18 changes: 0 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/buildkite/go-buildkite/v4 v4.10.0 h1:U3mYmDNLJqe+703Ztmf23ZA6eE/CnSsWevXyV9o9N4Q=
github.com/buildkite/go-buildkite/v4 v4.10.0/go.mod h1:DlebrRJqpZttXDjCW+MJ1QyW9AN++ZWt/UbPtKdbSSk=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
Expand All @@ -61,8 +59,6 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/huh/spinner v0.0.0-20240608175402-5b41f0b45136 h1:F1GgBu0ArS57Q9l1pJEVWWnpAWsZaeNn2/HAHzqm0eo=
github.com/charmbracelet/huh/spinner v0.0.0-20240608175402-5b41f0b45136/go.mod h1:1cf8ar2//4C/JYpK4/EHprHIKQDanErotkH8enQWG6g=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
Expand All @@ -71,24 +67,14 @@ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/teatest v0.0.0-20231215171016-7ba2b450712d h1:J6mdY8xl7YVGMSbPlqDcg64/J3m7wPuX1OWzPMWW4OA=
github.com/charmbracelet/x/exp/teatest v0.0.0-20231215171016-7ba2b450712d/go.mod h1:43J0pdacLjJQtomu7vU6RFZX3bn84toqNw7hjX8bhmM=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
Expand All @@ -104,8 +90,6 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7c
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
Expand Down Expand Up @@ -182,8 +166,6 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
Expand Down
51 changes: 33 additions & 18 deletions internal/io/confirm.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
package io

import (
"github.com/charmbracelet/huh"
"fmt"
"strings"

"github.com/buildkite/cli/v3/pkg/cmd/factory"
)

func Confirm(confirmed *bool, title string) error {
if *confirmed {
return nil
// Confirm prompts the user with a yes/no question.
// Returns true if the user confirmed, false otherwise.
//
// IMPORTANT: Commands using Confirm() must call f.SetGlobalFlags(cmd) in PreRunE.
// See factory.SetGlobalFlags() documentation for details.
//
// Usage:
//
// confirmed, err := io.Confirm(f, "Do the thing?")
// if err != nil {
// return err
// }
// if confirmed {
// // do the thing
// }
func Confirm(f *factory.Factory, prompt string) (bool, error) {
// Check if --yes flag is set
if f.SkipConfirm {
return true, nil
}

form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(title).
Affirmative("Yes").
Negative("No").
Value(confirmed),
),
)
// Check if --no-input flag is set
if f.NoInput {
return false, fmt.Errorf("interactive input required but --no-input is set")
}

err := form.Run()
fmt.Printf("%s [y/N]: ", prompt)

// no need to return error if ctrl-c
if err != nil && err == huh.ErrUserAborted {
return nil
response, err := ReadLine()
if err != nil {
return false, err
}

return err
response = strings.ToLower(response)
return response == "y" || response == "yes", nil
}
44 changes: 32 additions & 12 deletions internal/io/prompt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io

import "github.com/charmbracelet/huh"
import (
"fmt"
"strconv"
)

const (
typeOrganizationMessage = "Pick an organization"
Expand All @@ -9,7 +12,13 @@ const (

// PromptForOne will show the list of options to the user, allowing them to select one to return.
// It's possible for them to choose none or cancel the selection, resulting in an error.
func PromptForOne(resource string, options []string) (string, error) {
// If noInput is true, it will fail instead of prompting.
//
// For global flag support requirements, see the Confirm() function documentation.
func PromptForOne(resource string, options []string, noInput bool) (string, error) {
if noInput {
return "", fmt.Errorf("interactive input required but --no-input flag is set")
}
var message string
switch resource {
case "pipeline":
Expand All @@ -19,14 +28,25 @@ func PromptForOne(resource string, options []string) (string, error) {
default:
message = "Please select one of the options below"
}
selected := new(string)
err := huh.NewForm(huh.NewGroup(
huh.NewSelect[string]().
Title(message).
Options(
huh.NewOptions(options...)...,
).Value(selected),
),
).Run()
return *selected, err

if len(options) == 0 {
return "", fmt.Errorf("no options available")
}

fmt.Printf("%s:\n", message)
for i, option := range options {
fmt.Printf(" %d. %s\n", i+1, option)
}
fmt.Printf("Enter number (1-%d): ", len(options))

response, err := ReadLine()
if err != nil {
return "", err
}
num, err := strconv.Atoi(response)
if err != nil || num < 1 || num > len(options) {
return "", fmt.Errorf("invalid selection")
}

return options[num-1], nil
}
60 changes: 60 additions & 0 deletions internal/io/readline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io

import (
"bufio"
"os"
"strings"
"syscall"

"golang.org/x/term"
)

// ReadLine reads a line of input from stdin with terminal support.
// If running in a TTY, it uses x/term for better line editing (backspace, arrows, etc.).
// If not in a TTY (e.g., piped input), it falls back to bufio.
func ReadLine() (string, error) {
fd := int(os.Stdin.Fd())

// Check if we're in a TTY
if !term.IsTerminal(fd) {
// Not a TTY, use simple bufio
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(line), nil
}

// TTY - use x/term for better editing
oldState, err := term.MakeRaw(fd)
if err != nil {
// Fallback to bufio if raw mode fails
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(line), nil
}

terminal := term.NewTerminal(os.Stdin, "")
line, err := terminal.ReadLine()
_ = term.Restore(fd, oldState)

if err != nil {
return "", err
}

return strings.TrimSpace(line), nil
}

// ReadPassword reads a password from stdin without echoing.
// This is a convenience wrapper around term.ReadPassword.
func ReadPassword() (string, error) {
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", err
}
return string(passwordBytes), nil
}
2 changes: 1 addition & 1 deletion internal/pipeline/resolver/picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func PickOne(pipelines []pipeline.Pipeline) *pipeline.Pipeline {
names[i] = p.Name
}

chosen, err := io.PromptForOne("pipeline", names)
chosen, err := io.PromptForOne("pipeline", names, false)
if err != nil {
return nil
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/cmd/build/cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
func NewCmdBuildCancel(f *factory.Factory) *cobra.Command {
var web bool
var pipeline string
var confirmed bool

cmd := cobra.Command{
DisableFlagsInUseLine: true,
Expand All @@ -29,6 +28,8 @@ func NewCmdBuildCancel(f *factory.Factory) *cobra.Command {
Cancel the given build.
`),
PreRunE: func(cmd *cobra.Command, args []string) error {
f.SetGlobalFlags(cmd)

// Get the command's required and optional scopes
cmdScopes := scopes.GetCommandScopes(cmd)

Expand Down Expand Up @@ -64,16 +65,16 @@ func NewCmdBuildCancel(f *factory.Factory) *cobra.Command {
return err
}

err = bk_io.Confirm(&confirmed, fmt.Sprintf("Cancel build #%d on %s", bld.BuildNumber, bld.Pipeline))
confirmed, err := bk_io.Confirm(f, fmt.Sprintf("Cancel build #%d on %s", bld.BuildNumber, bld.Pipeline))
if err != nil {
return err
}

if confirmed {
return cancelBuild(cmd.Context(), bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), web, f)
} else {
return nil
}

return nil
},
}

Expand All @@ -83,7 +84,6 @@ func NewCmdBuildCancel(f *factory.Factory) *cobra.Command {

cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the build in a web browser after it has been cancelled.")
// Pipeline flag now inherited from parent command
cmd.Flags().BoolVarP(&confirmed, "yes", "y", false, "Skip the confirmation prompt. Useful if being used in automation/CI.")
cmd.Flags().SortFlags = false

return &cmd
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/build/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,6 @@ func fetchBuilds(cmd *cobra.Command, f *factory.Factory, org string, opts buildL
spinnerMsg += ")"

if format == output.FormatText && rawSinceConfirm >= maxBuildLimit {
var confirmed bool
prompt := fmt.Sprintf("Fetched %d more builds (%d total). Continue?", rawSinceConfirm, rawTotalFetched)
if filtersActive {
prompt = fmt.Sprintf(
Expand All @@ -307,7 +306,8 @@ func fetchBuilds(cmd *cobra.Command, f *factory.Factory, org string, opts buildL
)
}

if err := ConfirmFunc(&confirmed, prompt); err != nil {
confirmed, err := ConfirmFunc(f, prompt)
if err != nil {
return nil, err
}

Expand Down
13 changes: 5 additions & 8 deletions pkg/cmd/build/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,9 @@ func TestFetchBuildsConfirmationDeclineFirst(t *testing.T) {

confirmCalls := 0
origConfirm := ConfirmFunc
ConfirmFunc = func(confirmed *bool, title string) error {
ConfirmFunc = func(f *factory.Factory, prompt string) (bool, error) {
confirmCalls++
*confirmed = false
return nil
return false, nil
}
t.Cleanup(func() { ConfirmFunc = origConfirm })

Expand Down Expand Up @@ -257,14 +256,12 @@ func TestFetchBuildsConfirmationAcceptThenDecline(t *testing.T) {

confirmCalls := 0
origConfirm := ConfirmFunc
ConfirmFunc = func(confirmed *bool, title string) error {
ConfirmFunc = func(f *factory.Factory, prompt string) (bool, error) {
confirmCalls++
if confirmCalls == 1 {
*confirmed = true
} else {
*confirmed = false
return true, nil
}
return nil
return false, nil
}
t.Cleanup(func() { ConfirmFunc = origConfirm })

Expand Down
Loading