diff --git a/cmd/projects.go b/cmd/projects.go index 327cf81..def965c 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -22,6 +22,7 @@ type ProjectsService interface { ProjectListService New(ctx context.Context, body kernel.ProjectNewParams, opts ...option.RequestOption) (res *kernel.Project, err error) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.Project, err error) + Update(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (res *kernel.Project, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) } @@ -45,6 +46,14 @@ type ProjectsGetInput struct { Identifier string } +type ProjectsUpdateInput struct { + Identifier string + Name string + NameSet bool + Status string + StatusSet bool +} + type ProjectsDeleteInput struct { Identifier string } @@ -133,6 +142,50 @@ func (c ProjectsCmd) Get(ctx context.Context, in ProjectsGetInput) error { return nil } +func (c ProjectsCmd) Update(ctx context.Context, in ProjectsUpdateInput) error { + if !in.NameSet && !in.StatusSet { + return fmt.Errorf("must provide at least one of --name or --status") + } + + projectID, err := resolveProjectArg(ctx, c.projects, in.Identifier) + if err != nil { + return err + } + + inner := kernel.UpdateProjectRequestParam{} + if in.NameSet { + inner.Name = param.NewOpt(in.Name) + } + if in.StatusSet { + switch in.Status { + case "active": + inner.Status = kernel.UpdateProjectRequestStatusActive + case "archived": + inner.Status = kernel.UpdateProjectRequestStatusArchived + default: + return fmt.Errorf("--status must be one of: active, archived (got %q)", in.Status) + } + } + + project, err := c.projects.Update(ctx, projectID, kernel.ProjectUpdateParams{ + UpdateProjectRequest: inner, + }) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + pterm.Success.Printf("Updated project: %s (ID: %s)\n", project.Name, project.ID) + table := pterm.TableData{ + {"Field", "Value"}, + {"ID", project.ID}, + {"Name", project.Name}, + {"Status", string(project.Status)}, + {"Updated At", util.FormatLocal(project.UpdatedAt)}, + } + PrintTableNoPad(table, true) + return nil +} + func (c ProjectsCmd) Delete(ctx context.Context, in ProjectsDeleteInput) error { projectID, err := resolveProjectArg(ctx, c.projects, in.Identifier) if err != nil { @@ -280,6 +333,19 @@ func runProjectsGet(cmd *cobra.Command, args []string) error { return c.Get(cmd.Context(), ProjectsGetInput{Identifier: args[0]}) } +func runProjectsUpdate(cmd *cobra.Command, args []string) error { + c := getProjectsHandler(cmd) + name, _ := cmd.Flags().GetString("name") + status, _ := cmd.Flags().GetString("status") + return c.Update(cmd.Context(), ProjectsUpdateInput{ + Identifier: args[0], + Name: name, + NameSet: cmd.Flags().Changed("name"), + Status: status, + StatusSet: cmd.Flags().Changed("status"), + }) +} + func runProjectsDelete(cmd *cobra.Command, args []string) error { c := getProjectsHandler(cmd) return c.Delete(cmd.Context(), ProjectsDeleteInput{Identifier: args[0]}) @@ -364,6 +430,13 @@ var projectsGetCmd = &cobra.Command{ RunE: runProjectsGet, } +var projectsUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a project (rename or change status)", + Args: cobra.ExactArgs(1), + RunE: runProjectsUpdate, +} + var projectsDeleteCmd = &cobra.Command{ Use: "delete ", Short: "Delete a project", @@ -418,9 +491,13 @@ func init() { projectsLimitsCmd.AddCommand(projectsLimitsGetCmd) projectsLimitsCmd.AddCommand(projectsLimitsSetCmd) + projectsUpdateCmd.Flags().String("name", "", "New project name") + projectsUpdateCmd.Flags().String("status", "", "New project status (active, archived)") + projectsCmd.AddCommand(projectsListCmd) projectsCmd.AddCommand(projectsCreateCmd) projectsCmd.AddCommand(projectsGetCmd) + projectsCmd.AddCommand(projectsUpdateCmd) projectsCmd.AddCommand(projectsDeleteCmd) projectsCmd.AddCommand(projectsLimitsCmd) projectsCmd.AddCommand(projectsGetLimitsCompatCmd) diff --git a/cmd/projects_test.go b/cmd/projects_test.go index 7ae7347..51dd15b 100644 --- a/cmd/projects_test.go +++ b/cmd/projects_test.go @@ -42,6 +42,7 @@ type FakeProjectsService struct { ListFunc func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) NewFunc func(ctx context.Context, body kernel.ProjectNewParams, opts ...option.RequestOption) (*kernel.Project, error) GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.Project, error) + UpdateFunc func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error } @@ -66,6 +67,13 @@ func (f *FakeProjectsService) Get(ctx context.Context, id string, opts ...option return &kernel.Project{ID: id, Name: "default"}, nil } +func (f *FakeProjectsService) Update(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) { + if f.UpdateFunc != nil { + return f.UpdateFunc(ctx, id, body, opts...) + } + return &kernel.Project{ID: id, Name: body.UpdateProjectRequest.Name.Value, Status: kernel.ProjectStatusActive}, nil +} + func (f *FakeProjectsService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error { if f.DeleteFunc != nil { return f.DeleteFunc(ctx, id, opts...) @@ -92,6 +100,49 @@ func (f *FakeProjectLimitsService) Update(ctx context.Context, id string, body k return &kernel.ProjectLimits{}, nil } +func TestProjectsUpdate_RenamesAndUpdatesStatus(t *testing.T) { + captureProjectsOutput(t) + + var captured kernel.ProjectUpdateParams + fakeProjects := &FakeProjectsService{ + UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) { + captured = body + return &kernel.Project{ID: id, Name: "renamed", Status: kernel.ProjectStatusArchived}, nil + }, + } + c := ProjectsCmd{projects: fakeProjects, limits: &FakeProjectLimitsService{}} + + err := c.Update(context.Background(), ProjectsUpdateInput{ + Identifier: "a12345678901234567890123", + Name: "renamed", + NameSet: true, + Status: "archived", + StatusSet: true, + }) + assert.NoError(t, err) + assert.True(t, captured.UpdateProjectRequest.Name.Valid()) + assert.Equal(t, "renamed", captured.UpdateProjectRequest.Name.Value) + assert.Equal(t, kernel.UpdateProjectRequestStatusArchived, captured.UpdateProjectRequest.Status) +} + +func TestProjectsUpdate_RequiresAtLeastOneFlag(t *testing.T) { + c := ProjectsCmd{projects: &FakeProjectsService{}, limits: &FakeProjectLimitsService{}} + err := c.Update(context.Background(), ProjectsUpdateInput{Identifier: "a12345678901234567890123"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "at least one of --name or --status") +} + +func TestProjectsUpdate_RejectsUnknownStatus(t *testing.T) { + c := ProjectsCmd{projects: &FakeProjectsService{}, limits: &FakeProjectLimitsService{}} + err := c.Update(context.Background(), ProjectsUpdateInput{ + Identifier: "a12345678901234567890123", + Status: "deleted", + StatusSet: true, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "active, archived") +} + func TestProjectsLimitsGet_DefaultOutput(t *testing.T) { buf := captureProjectsOutput(t) limits := &kernel.ProjectLimits{