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
11 changes: 11 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
32 changes: 32 additions & 0 deletions shared/prompt/confirm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package prompt provides user interaction utilities.
package prompt

import (
"bufio"
"io"
"strings"
)

// Confirm prompts the user for confirmation and returns true if they answer yes.
// It reads a single line from stdin and returns true if the answer is "y" or "Y".
func Confirm(stdin io.Reader) (bool, error) {
scanner := bufio.NewScanner(stdin)
if scanner.Scan() {
answer := strings.TrimSpace(scanner.Text())
return answer == "y" || answer == "Y", nil
}
if err := scanner.Err(); err != nil {
return false, err
}
// EOF with no input means no confirmation
return false, nil
}

// ConfirmOrForce returns true if force is true, or if the user confirms interactively.
// If force is true, it returns immediately without reading from stdin.
func ConfirmOrForce(force bool, stdin io.Reader) (bool, error) {
if force {
return true, nil
}
return Confirm(stdin)
}
113 changes: 113 additions & 0 deletions shared/prompt/confirm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package prompt

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConfirm(t *testing.T) {
tests := []struct {
name string
input string
want bool
wantErr bool
}{
{
name: "lowercase y confirms",
input: "y\n",
want: true,
},
{
name: "uppercase Y confirms",
input: "Y\n",
want: true,
},
{
name: "yes does not confirm (only y)",
input: "yes\n",
want: false,
},
{
name: "n does not confirm",
input: "n\n",
want: false,
},
{
name: "empty input does not confirm",
input: "\n",
want: false,
},
{
name: "whitespace around y confirms",
input: " y \n",
want: true,
},
{
name: "EOF without input does not confirm",
input: "",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Confirm(strings.NewReader(tt.input))
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestConfirmOrForce(t *testing.T) {
tests := []struct {
name string
force bool
input string
want bool
wantErr bool
}{
{
name: "force bypasses confirmation",
force: true,
input: "", // Not read when force is true
want: true,
},
{
name: "without force, y confirms",
force: false,
input: "y\n",
want: true,
},
{
name: "without force, n does not confirm",
force: false,
input: "n\n",
want: false,
},
{
name: "without force, empty does not confirm",
force: false,
input: "\n",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ConfirmOrForce(tt.force, strings.NewReader(tt.input))
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
13 changes: 6 additions & 7 deletions tools/cfl/internal/cmd/page/delete.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package page

import (
"bufio"
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/open-cli-collective/atlassian-go/prompt"

"github.com/open-cli-collective/confluence-cli/internal/cmd/root"
)

Expand Down Expand Up @@ -55,13 +56,11 @@ func runDelete(pageID string, opts *deleteOptions) error {
fmt.Printf("About to delete page: %s (ID: %s)\n", page.Title, page.ID)
fmt.Print("Are you sure? [y/N]: ")

scanner := bufio.NewScanner(opts.Stdin)
var confirm string
if scanner.Scan() {
confirm = scanner.Text()
confirmed, err := prompt.Confirm(opts.Stdin)
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}

if confirm != "y" && confirm != "Y" {
if !confirmed {
fmt.Println("Deletion cancelled.")
return nil
}
Expand Down
16 changes: 13 additions & 3 deletions tools/jtk/internal/cmd/issues/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/spf13/cobra"

"github.com/open-cli-collective/atlassian-go/prompt"

"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
)

Expand Down Expand Up @@ -35,9 +37,17 @@ func runDelete(opts *root.Options, issueKey string, force bool) error {
v := opts.View()

if !force {
v.Warning("This will permanently delete issue %s. This action cannot be undone.", issueKey)
v.Info("Use --force to skip this confirmation.")
return fmt.Errorf("deletion cancelled (use --force to confirm)")
fmt.Printf("This will permanently delete issue %s. This action cannot be undone.\n", issueKey)
fmt.Print("Are you sure? [y/N]: ")

confirmed, err := prompt.Confirm(opts.Stdin)
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
if !confirmed {
v.Info("Deletion cancelled.")
return nil
}
}

client, err := opts.APIClient()
Expand Down
Loading