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
9 changes: 9 additions & 0 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ func runAgentRemove(_ *cobra.Command, args []string) error {
return err
}

ok, err := confirmDestructive(fmt.Sprintf("Remove agent %q (%s)?", a.Name, a.FilePath))
if err != nil {
return err
}
if !ok {
fmt.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}

if err := os.Remove(a.FilePath); err != nil {
return fmt.Errorf("remove %s: %w", a.FilePath, err)
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ var checkpointDeleteCmd = &cobra.Command{
Short: "Delete a named checkpoint",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ok, err := confirmDestructive(fmt.Sprintf("Delete checkpoint %q?", args[0]))
if err != nil {
return err
}
if !ok {
cmd.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}
if err := session.DeleteNamedCheckpoint(args[0]); err != nil {
return err
}
Expand Down
18 changes: 18 additions & 0 deletions cmd/confirm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cmd

import "testing"

func TestParseConfirm(t *testing.T) {
yes := []string{"y", "Y", "yes", "Yes", "YES", " y ", "\tyes\n"}
for _, in := range yes {
if !parseConfirm(in) {
t.Errorf("parseConfirm(%q) = false, want true", in)
}
}
no := []string{"", "n", "N", "no", "No", "NO", "maybe", "1", "true", "yess", " yyy"}
for _, in := range no {
if parseConfirm(in) {
t.Errorf("parseConfirm(%q) = true, want false", in)
}
}
}
8 changes: 8 additions & 0 deletions cmd/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ var credentialsRemoveCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ok, err := confirmDestructive(fmt.Sprintf("Remove stored API key(s) for %q from %s?", args[0], graycodeconfig.CredentialStoreName()))
if err != nil {
return err
}
if !ok {
cmd.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}
removed, err := graycodeconfig.RemoveStoredCredential(ctx, args[0])
if err != nil {
return err
Expand Down
8 changes: 8 additions & 0 deletions cmd/learn_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ var learnClearCmd = &cobra.Command{
cmd.Println(auditTint("no lessons to clear", textMuted))
return nil
}
ok, err := confirmDestructive(fmt.Sprintf("Remove all %d lesson(s)?", n))
if err != nil {
return err
}
if !ok {
cmd.Println(auditTint("Cancelled.", textMuted))
return nil
}
si.Clear()
cmd.Println(auditTint("cleared "+strconv.Itoa(n)+" lesson(s)", textPrimary))
return nil
Expand Down
8 changes: 8 additions & 0 deletions cmd/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ var permissionsResetCmd = &cobra.Command{
cmd.Println(auditTint("No persisted permission rules.", textMuted))
return nil
}
ok, err := confirmDestructive("Remove all persisted permission rules?")
if err != nil {
return err
}
if !ok {
cmd.Println(auditTint("Cancelled.", textMuted))
return nil
}
if err := store.Save(); err != nil {
return err
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/skills_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ var skillsRemoveCmd = &cobra.Command{
Short: "Remove an installed skill",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ok, err := confirmDestructive(fmt.Sprintf("Remove skill %q?", args[0]))
if err != nil {
return err
}
if !ok {
fmt.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}
if err := plugin.Remove(args[0]); err != nil {
return err
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/taste.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ func runTasteReset(_ *cobra.Command, _ []string) error {
}

projectID := getProjectID()
ok, err := confirmDestructive(fmt.Sprintf("Clear all taste preferences for project %q? This cannot be undone.", projectID))
if err != nil {
return err
}
if !ok {
fmt.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}
if err := store.Delete(projectID); err != nil {
return fmt.Errorf("reset profile: %w", err)
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/trust.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ var trustRemoveCmd = &cobra.Command{
if err != nil {
return err
}
ok, err := confirmDestructive(fmt.Sprintf("Remove trust for %q?", path))
if err != nil {
return err
}
if !ok {
cmd.Printf("%s\n", auditTint("Cancelled.", textMuted))
return nil
}
if err := s.Untrust(path); err != nil {
return err
}
Expand Down
26 changes: 26 additions & 0 deletions cmd/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"os"
"strings"

"golang.org/x/term"
)
Expand All @@ -19,6 +20,31 @@ func CanPrompt() bool {
return !quietFlag && stdinIsTerminal()
}

// confirmDestructive prompts the user to confirm a destructive, hard-to-undo
// operation. It returns true only on an explicit "y"/"yes". When the user is
// not interactive (--quiet, piped stdin, or no TTY), it returns true so
// scripts and automation are never blocked. A declined prompt returns false
// with no error; callers should abort the operation and exit cleanly.
func confirmDestructive(prompt string) (bool, error) {
if !CanPrompt() {
return true, nil
}
input := openPromptInput()
defer input.close()
answer, err := input.readLine(prompt + " [y/N] ")
if err != nil {
return false, err
}
return parseConfirm(answer), nil
}

// parseConfirm reports whether a user answer is an explicit affirmative
// ("y" / "yes", case-insensitive, trimmed). Anything else declines.
func parseConfirm(answer string) bool {
answer = strings.ToLower(strings.TrimSpace(answer))
return answer == "y" || answer == "yes"
}

// ShouldColor returns true when colored output is appropriate for stdout.
// Respects --quiet, NO_COLOR, FORCE_COLOR, and TTY state.
func ShouldColor() bool {
Expand Down
Loading