feat(control-plane): confirm destructive org/project/repo delete · Entire
feat(control-plane): confirm destructive org/project/repo delete
966aa98→main·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
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"
}
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
``
```Mcmd/entire/cli/corecmd.go+89
4 unmodified lines
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// TestConfirmControlPlaneDeletion covers the non-TTY decision paths of the // destructive-delete gate. The interactive form path needs a real terminal and // is left to manual/e2e coverage. func TestConfirmControlPlaneDeletion(t *testing.T) { t.Parallel()
// --force proceeds without prompting (no TTY needed). var buf bytes.Buffer proceed, err := confirmControlPlaneDeletion(t.Context(), &buf, "org acme (01J)", true, false) if err != nil || !proceed { t.Fatalf("force: got (proceed=%v, err=%v), want (true, nil)", proceed, err) }
// Non-interactive without --force must refuse, not delete unprompted. buf.Reset() proceed, err = confirmControlPlaneDeletion(t.Context(), &buf, "org acme (01J)", false, false) if err == nil { t.Fatalf("non-interactive without --force: expected error, got nil (proceed=%v)", proceed) } if proceed { t.Fatal("non-interactive without --force: must not proceed") } if !strings.Contains(err.Error(), "--force") { t.Fatalf("error should mention --force, got: %v", err) } if !strings.Contains(err.Error(), "org acme") { t.Fatalf("error should name the target, got: %v", err) }
// An already-cancelled context is a clean cancel: no prompt, no error. ctx, cancel := context.WithCancel(context.Background()) cancel() buf.Reset() proceed, err = confirmControlPlaneDeletion(ctx, &buf, "org acme (01J)", false, true) if err != nil || proceed { t.Fatalf("cancelled ctx: got (proceed=%v, err=%v), want (false, nil)", proceed, err) } }
// TestFetchAllPages walks a multi-page source, stops on the empty cursor, // and errors rather than looping when the server fails to advance. func TestFetchAllPages(t *testing.T) {
```Mcmd/entire/cli/corecmd_test.go+40
93 unmodified lines
94
95
96
97
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
102
103
104
105
106
107
108
109
110
111
112
113
return &cobra.Command{
cmd := &cobra.Command{
Use: "delete <org>",
Short: "Delete an organization by name or ULID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
orgID, err := resolveOrgRef(ctx, c, args[0])
if err != nil {
return err
}
if err := c.DeleteOrg(ctx, coreapi.DeleteOrgParams{OrgId: orgID}); err != nil {
return err
}
cmd.Printf("Deleted org %s\n", resolvedRefLabel(args[0], orgID))
return nil
})
return runControlPlaneDelete(cmd, "org", args[0],
func(ctx context.Context, c *coreapi.Client) (string, error) {
return resolveOrgRef(ctx, c, args[0])
},
func(ctx context.Context, c *coreapi.Client, id string) error {
return c.DeleteOrg(ctx, coreapi.DeleteOrgParams{OrgId: id})
})
},
}
addForceFlag(cmd)
return cmd
}
172 unmodified lines
173
174
175
176
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
181
182
183
184
185
186
187
188
189
190
191
192
193
194
return &cobra.Command{
cmd := &cobra.Command{
Use: "delete <project>",
Short: "Delete a project by name or ULID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
projID, err := resolveProjectRef(ctx, c, args[0])
if err != nil {
return err
}
if err := c.DeleteProject(ctx, coreapi.DeleteProjectParams{ProjectId: projID}); err != nil {
return err
}
cmd.Printf("Deleted project %s\n", resolvedRefLabel(args[0], projID))
return nil
})
return runControlPlaneDelete(cmd, "project", args[0],
func(ctx context.Context, c *coreapi.Client) (string, error) {
return resolveProjectRef(ctx, c, args[0])
},
func(ctx context.Context, c *coreapi.Client, id string) error {
return c.DeleteProject(ctx, coreapi.DeleteProjectParams{ProjectId: id})
})
},
}
addForceFlag(cmd)
return cmd
}
182 unmodified lines
183
184
185
186
187
188
189
190
191
192
193
194
195
196
186
187
188
189
190
191
192
193
194
195
196
197
198
199
return &cobra.Command{
cmd := &cobra.Command{
Use: "delete <repo>",
Short: "Delete a repository by name or ULID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
repoID, err := resolveRepoRef(ctx, c, args[0], project)
if err != nil {
return err
}
if err := c.DeleteRepo(ctx, coreapi.DeleteRepoParams{RepoId: repoID}); err != nil {
return err
}
cmd.Printf("Deleted repo %s\n", resolvedRefLabel(args[0], repoID))
return nil
})
return runControlPlaneDelete(cmd, "repo", args[0],
func(ctx context.Context, c *coreapi.Client) (string, error) {
return resolveRepoRef(ctx, c, args[0], project)
},
func(ctx context.Context, c *coreapi.Client, id string) error {
return c.DeleteRepo(ctx, coreapi.DeleteRepoParams{RepoId: id})
})
},
}
bindRepoProjectFlag(cmd, &project)
addForceFlag(cmd)
return cmd
}
172 unmodified lines
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199