Skip to content

Commit 33fe02a

Browse files
Merge pull request #370 from depot/feat/add-projects-delete
Add `depot projects delete` command
2 parents a7578e6 + 8ecfe75 commit 33fe02a

File tree

5 files changed

+178
-16
lines changed

5 files changed

+178
-16
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Official CLI for [Depot](https://depot.dev) - you can use the CLI to build Docke
2828
- [`depot list builds`](#depot-list-builds)
2929
- [`depot projects`](#depot-projects)
3030
- [`depot projects create`](#depot-projects-create)
31+
- [`depot projects delete`](#depot-projects-delete)
3132
- [`depot projects list`](#depot-projects-list)
3233
- [`depot init`](#depot-init)
3334
- [`depot login`](#depot-login)
@@ -428,6 +429,31 @@ depot projects create --organization your-org-id --region us-west-2 --cache-stor
428429
| `cache-storage-policy` | Build cache to keep per architecture in GB (default: 50) |
429430
| `token` | Depot API token |
430431

432+
#### `depot projects delete`
433+
434+
Delete a Depot project. If no project ID is specified, the command will display an interactive list of projects to choose from.
435+
436+
**Example**
437+
438+
```shell
439+
# Delete a specific project
440+
depot projects delete --project-id your-project-id
441+
442+
# Delete a project interactively
443+
depot projects delete
444+
445+
# Delete a project without confirmation prompt
446+
depot projects delete --project-id your-project-id --yes
447+
```
448+
449+
#### Flags for `projects delete`
450+
451+
| Name | Description |
452+
| -------------- | ---------------------------------------------- |
453+
| `project-id` | The ID of the project to delete |
454+
| `yes` | Confirm deletion without interactive prompt |
455+
| `token` | Depot API token |
456+
431457
#### `depot projects list`
432458

433459
List Depot projects. This command is functionally identical to `depot list projects` and provides an interactive listing of your Depot projects.

pkg/cmd/projects/delete.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package projects
2+
3+
import (
4+
"fmt"
5+
6+
v1 "buf.build/gen/go/depot/api/protocolbuffers/go/depot/core/v1"
7+
"connectrpc.com/connect"
8+
"github.com/charmbracelet/huh"
9+
"github.com/depot/cli/pkg/api"
10+
"github.com/depot/cli/pkg/helpers"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func NewCmdDelete() *cobra.Command {
15+
var (
16+
token string
17+
projectID string
18+
confirm bool
19+
)
20+
21+
cmd := &cobra.Command{
22+
Use: "delete",
23+
Aliases: []string{"d"},
24+
Args: cobra.MaximumNArgs(1),
25+
Short: "Delete a project",
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
ctx := cmd.Context()
28+
29+
// Resolve token first
30+
token, err := helpers.ResolveToken(cmd.Context(), token)
31+
if err != nil {
32+
return err
33+
}
34+
35+
if projectID == "" {
36+
// List projects
37+
projects, err := helpers.RetrieveProjects(ctx, token)
38+
if err != nil {
39+
return err
40+
}
41+
42+
// Select project to delete
43+
projectID, err = helpers.SelectProject(projects.Projects)
44+
if err != nil {
45+
return err
46+
}
47+
}
48+
49+
// If not confirmed through flag already, ask for confirmation interactively
50+
if !confirm {
51+
err := huh.NewConfirm().
52+
Title(fmt.Sprintf("Are you sure you want to delete project %s?", projectID)).
53+
Affirmative("Yes!").
54+
Negative("No.").
55+
Value(&confirm).
56+
Run()
57+
if err != nil {
58+
return err
59+
}
60+
}
61+
62+
// If not confirmed, exit
63+
if !confirm {
64+
fmt.Println("Cancelling project deletion...")
65+
return nil
66+
}
67+
68+
// Delete project
69+
client := api.NewSDKProjectsClient()
70+
req := v1.DeleteProjectRequest{
71+
ProjectId: projectID,
72+
}
73+
74+
_, err = client.DeleteProject(ctx, api.WithAuthentication(connect.NewRequest(&req), token))
75+
if err != nil {
76+
return err
77+
}
78+
79+
fmt.Printf("Successfully deleted project %s\n", projectID)
80+
return nil
81+
},
82+
}
83+
84+
flags := cmd.Flags()
85+
flags.StringVarP(&token, "token", "t", "", "Depot API token")
86+
flags.StringVarP(&projectID, "project-id", "p", "", "The ID of the project to delete")
87+
flags.BoolVarP(&confirm, "yes", "y", false, "Confirm deletion")
88+
89+
return cmd
90+
}

pkg/cmd/projects/projects.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func NewCmdProjects() *cobra.Command {
1616
}
1717

1818
cmd.AddCommand(NewCmdCreate())
19+
cmd.AddCommand(NewCmdDelete())
1920
cmd.AddCommand(list.NewCmdProjects("list", "ls"))
2021

2122
return cmd

pkg/config/config.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ func NewConfig() error {
1717
viper.SetEnvPrefix("DEPOT")
1818
viper.AutomaticEnv()
1919

20-
err = viper.ReadInConfig()
21-
return fmt.Errorf("unable to read config file: %v", err)
20+
if err := viper.ReadInConfig(); err != nil {
21+
// It's okay if the config file doesn't exist
22+
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
23+
return fmt.Errorf("unable to read config file: %v", err)
24+
}
25+
}
26+
return nil
2227
}
2328

2429
func GetApiToken() string {

pkg/helpers/project.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"connectrpc.com/connect"
1212
"github.com/charmbracelet/bubbles/list"
1313
tea "github.com/charmbracelet/bubbletea"
14+
"github.com/charmbracelet/huh"
1415
"github.com/charmbracelet/lipgloss"
1516
"github.com/depot/cli/pkg/api"
1617
"github.com/depot/cli/pkg/project"
@@ -124,25 +125,23 @@ func (p *SelectedProject) SaveAs(configFilePath string) error {
124125
}
125126

126127
func ProjectExists(ctx context.Context, token, projectID string) (*SelectedProject, error) {
127-
client := api.NewProjectsClient()
128-
req := cliv1beta1.ListProjectsRequest{}
129-
projects, err := client.ListProjects(ctx, api.WithAuthentication(connect.NewRequest(&req), token))
128+
projects, err := RetrieveProjects(ctx, token)
130129
if err != nil {
131130
return nil, err
132131
}
133132

134133
// In the case that the user specified a project id on the command line with `--project`,
135134
// we check to see if the project exists. If it does not, we return an error.
136135
var selectedProject *cliv1beta1.ListProjectsResponse_Project
137-
for _, p := range projects.Msg.Projects {
136+
for _, p := range projects.Projects {
138137
if p.Id == projectID {
139138
selectedProject = p
140139
break
141140
}
142141
}
143142

144143
if selectedProject == nil {
145-
return nil, fmt.Errorf("Project with ID %s not found", projectID)
144+
return nil, fmt.Errorf("project with ID %s not found", projectID)
146145
}
147146

148147
return &SelectedProject{
@@ -153,32 +152,29 @@ func ProjectExists(ctx context.Context, token, projectID string) (*SelectedProje
153152
}
154153

155154
func InitializeProject(ctx context.Context, token, projectID string) (*SelectedProject, error) {
156-
client := api.NewProjectsClient()
157-
158-
req := cliv1beta1.ListProjectsRequest{}
159-
projects, err := client.ListProjects(ctx, api.WithAuthentication(connect.NewRequest(&req), token))
155+
projects, err := RetrieveProjects(ctx, token)
160156
if err != nil {
161157
return nil, err
162158
}
163159

164-
if len(projects.Msg.Projects) == 0 {
165-
return nil, fmt.Errorf("No projects found. Please create a project first.")
160+
if len(projects.Projects) == 0 {
161+
return nil, fmt.Errorf("no projects found. Please create a project first")
166162
}
167163

168164
// If we're not in a terminal, just print the projects and exit as we need
169165
// user intervention to pick a project.
170166
if !IsTerminal() {
171-
err := printProjectsCSV(projects.Msg.Projects)
167+
err := printProjectsCSV(projects.Projects)
172168
if err != nil {
173169
return nil, err
174170
}
175171
return nil, fmt.Errorf("missing project ID; please run `depot init` or `depot build --project <id>`")
176172
}
177173

178174
if projectID == "" {
179-
projectID, err = chooseProjectID(projects.Msg)
175+
projectID, err = chooseProjectID(projects)
180176
if err != nil {
181-
return nil, fmt.Errorf("No project selected; please run `depot init`")
177+
return nil, fmt.Errorf("no project selected; please run `depot init`")
182178
}
183179
}
184180

@@ -288,3 +284,47 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
288284
func (m model) View() string {
289285
return docStyle.Render(m.list.View())
290286
}
287+
288+
// SelectProject allows selecting a project using huh library
289+
func SelectProject(projects []*cliv1beta1.ListProjectsResponse_Project) (string, error) {
290+
if len(projects) == 0 {
291+
return "", fmt.Errorf("no projects found")
292+
}
293+
294+
var options []huh.Option[string]
295+
for _, p := range projects {
296+
options = append(options, huh.NewOption(fmt.Sprintf("%s (%s)", p.Name, p.OrgName), p.Id))
297+
}
298+
299+
var selectedID string
300+
form := huh.NewForm(
301+
huh.NewGroup(
302+
huh.NewSelect[string]().
303+
Title("Choose a project").
304+
Options(options...).
305+
Value(&selectedID),
306+
),
307+
)
308+
309+
err := form.Run()
310+
if err != nil {
311+
return "", fmt.Errorf("error selecting project: %w", err)
312+
}
313+
314+
if selectedID == "" {
315+
return "", fmt.Errorf("no project selected")
316+
}
317+
318+
return selectedID, nil
319+
}
320+
321+
// RetrieveProjects calls the API to get the list of projects
322+
func RetrieveProjects(ctx context.Context, token string) (*cliv1beta1.ListProjectsResponse, error) {
323+
client := api.NewProjectsClient()
324+
req := cliv1beta1.ListProjectsRequest{}
325+
resp, err := client.ListProjects(ctx, api.WithAuthentication(connect.NewRequest(&req), token))
326+
if err != nil {
327+
return nil, fmt.Errorf("error retrieving projects: %w", err)
328+
}
329+
return resp.Msg, nil
330+
}

0 commit comments

Comments
 (0)