feat(control-plane): confirm destructive org/project/repo delete · Entire
feat(control-plane): confirm destructive org/project/repo delete
15df8e9·
toothbrush·3w ago·5 files·+157 added/-35 removed
org/project/repo delete called the destructive API immediately after resolving the ref — a name typo that resolves to a real resource (or a wrong-but-valid ULID) deleted silently. Gate each behind a confirmation prompt (the existing trail-delete pattern) with a --force/-f bypass and --yes/-y alias; non-interactive runs refuse without --force rather than deleting unprompted. Shared body extracted to runControlPlaneDelete.
Addresses trail #642 finding.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
5201fa5dc67cView transcript
[?
Control Plane Delete and Revoke SafetyClaude Code·Opus 4.8[1m]·2 steps](/content/gh/entireio/cli/session/8e196be2-884b-4372-a672-2e81e2857e10#timeline-5201fa5dc67c/index.html)
Changes
5
cmd/entire/cli
Mcorecmd.go+89
Mcorecmd_test.go+40
Morg.go+10/-12
Mproject.go+10/-12
Mrepo.go+8/-11
7 unmodified lines
8
9
10
11
12
13
14
15
16
17
18
19
27 unmodified lines
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
7 unmodified lines
"io"
"strings"
"charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/spf13/cobra"
"github.com/entireio/cli/cmd/entire/cli/auth"
"github.com/entireio/cli/cmd/entire/cli/interactive"
"github.com/entireio/cli/internal/coreapi"
27 unmodified lines
return err == nil && v
}
// addForceFlag registers the standard confirmation bypass on a destructive
// control-plane command: --force/-f, with --yes/-y as an alias. Either skips
// the prompt. Read the combined value with forceRequested.
func addForceFlag(cmd *cobra.Command) {
cmd.Flags().BoolP("force", "f", false, "Skip the confirmation prompt")
cmd.Flags().BoolP("yes", "y", false, "Skip the confirmation prompt (alias for --force)")
}
// forceRequested reports whether the delete should skip its confirmation
// prompt, i.e. --force or its --yes alias was set.
func forceRequested(cmd *cobra.Command) bool {
force, ferr := cmd.Flags().GetBool("force")
yes, yerr := cmd.Flags().GetBool("yes")
return (ferr == nil && force) || (yerr == nil && yes)
}
// runControlPlaneDelete is the shared body of the destructive `delete` verbs
// (org/project/repo). It resolves the target ref to a ULID, gates on a
// confirmation prompt (bypassed by --force/--yes), deletes, and reports the
// resolved identifier. noun names the resource ("org"); ref is the user's
// original argument, shown alongside the resolved ULID. resolve and del isolate
// the per-resource API calls.
func runControlPlaneDelete(
cmd *cobra.Command,
noun, ref string,
resolve func(context.Context, *coreapi.Client) (string, error),
del func(context.Context, *coreapi.Client, string) error,
) error {
force := forceRequested(cmd)
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
id, err := resolve(ctx, c)
if err != nil {
return err
}
label := noun + " " + resolvedRefLabel(ref, id)
proceed, err := confirmControlPlaneDeletion(ctx, cmd.OutOrStdout(), label, force, interactive.CanPromptInteractively())
if err != nil {
return err
}
if !proceed {
return nil
}
if err := del(ctx, c, id); err != nil {
return err
}
cmd.Printf("Deleted %s\n", label)
return nil
})
}
// confirmControlPlaneDeletion gates a destructive control-plane delete. With
// force it proceeds silently. Otherwise it requires an interactive terminal:
// with none it refuses (returns an error) rather than deleting unprompted; with
// one it shows a confirmation form. canPrompt is passed in (not queried) so the
// decision is unit-testable without a TTY. label is the human description of
// the target, e.g. `org acme (01J…)`.
func confirmControlPlaneDeletion(ctx context.Context, w io.Writer, label string, force, canPrompt bool) (bool, error) {
if force {
return true, nil
}
if !canPrompt {
return false, fmt.Errorf("refusing to delete %s without confirmation; pass --force", label)
}
// huh opens the TTY during form startup regardless of context state, so
// guard explicitly to honor an already-cancelled command context.
if ctx.Err() != nil {
return false, nil //nolint:nilerr // cancelled context is a clean skip, not an error
}
confirmed := false
form := NewAccessibleForm(
huh.NewGroup(huh.NewConfirm().Title(fmt.Sprintf("Delete %s?", label)).Value(&confirmed)),
)
if err := form.RunWithContext(ctx); err != nil {
// A user abort (Esc) or context cancel (Ctrl+C) is a clean cancel, not
// an error — mirror confirmTrailDeletion.
if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, context.Canceled) {
return false, nil
}
return false, fmt.Errorf("deletion prompt: %w", err)
}
if !confirmed {
fmt.Fprintln(w, "Deletion cancelled.")
return false, nil
}
return true, nil
}
// runCoreList fetches a slice via fn and renders it as an aligned table
// (default) or the raw wire JSON (--json). headers names the columns; row
// maps one item to its cells in the same order. The human view keeps the