Skip to content
Open
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
63 changes: 63 additions & 0 deletions cmd/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -48,6 +49,12 @@ type ProjectsGetInput struct {
Output string
}

type ProjectsUpdateInput struct {
Identifier string
Name string
Output string
}

type ProjectsDeleteInput struct {
Identifier string
}
Expand Down Expand Up @@ -155,6 +162,40 @@ func (c ProjectsCmd) Get(ctx context.Context, in ProjectsGetInput) error {
return nil
}

func (c ProjectsCmd) Update(ctx context.Context, in ProjectsUpdateInput) error {
if err := validateJSONOutput(in.Output); err != nil {
return err
}
if in.Name == "" {
return util.RequiredFlag("--name", "<name>")
}

projectID, err := resolveProjectArg(ctx, c.projects, in.Identifier)
if err != nil {
return err
}

project, err := c.projects.Update(ctx, projectID, kernel.ProjectUpdateParams{
UpdateProjectRequest: kernel.UpdateProjectRequestParam{
Name: kernel.String(in.Name),
},
})
if err != nil {
return util.CleanedUpSdkError{Err: err}
}

if in.Output == "json" {
if project == nil {
fmt.Println("null")
return nil
}
return util.PrintPrettyJSON(project)
}

pterm.Success.Printf("Updated project: %s (ID: %s)\n", project.Name, project.ID)
return nil
}

func (c ProjectsCmd) Delete(ctx context.Context, in ProjectsDeleteInput) error {
projectID, err := resolveProjectArg(ctx, c.projects, in.Identifier)
if err != nil {
Expand Down Expand Up @@ -297,6 +338,17 @@ func runProjectsGet(cmd *cobra.Command, args []string) error {
return c.Get(cmd.Context(), ProjectsGetInput{Identifier: args[0], Output: output})
}

func runProjectsUpdate(cmd *cobra.Command, args []string) error {
c := getProjectsHandler(cmd)
name, _ := cmd.Flags().GetString("name")
output, _ := cmd.Flags().GetString("output")
return c.Update(cmd.Context(), ProjectsUpdateInput{
Identifier: args[0],
Name: name,
Output: output,
})
}

func runProjectsDelete(cmd *cobra.Command, args []string) error {
c := getProjectsHandler(cmd)
return c.Delete(cmd.Context(), ProjectsDeleteInput{Identifier: args[0]})
Expand Down Expand Up @@ -371,6 +423,13 @@ var projectsGetCmd = &cobra.Command{
RunE: runProjectsGet,
}

var projectsUpdateCmd = &cobra.Command{
Use: "update <id-or-name>",
Short: "Update a project",
Args: cobra.ExactArgs(1),
RunE: runProjectsUpdate,
}

var projectsDeleteCmd = &cobra.Command{
Use: "delete <id-or-name>",
Short: "Delete a project",
Expand Down Expand Up @@ -419,6 +478,9 @@ var projectsSetLimitsCompatCmd = &cobra.Command{
func init() {
addJSONOutputFlag(projectsListCmd)
addJSONOutputFlag(projectsGetCmd)
addJSONOutputFlag(projectsUpdateCmd)
projectsUpdateCmd.Flags().String("name", "", "New project name (required)")
_ = projectsUpdateCmd.MarkFlagRequired("name")
addJSONOutputFlag(projectsLimitsGetCmd)
addProjectsLimitsSetFlags(projectsLimitsSetCmd)
addJSONOutputFlag(projectsGetLimitsCompatCmd)
Expand All @@ -430,6 +492,7 @@ func init() {
projectsCmd.AddCommand(projectsListCmd)
projectsCmd.AddCommand(projectsCreateCmd)
projectsCmd.AddCommand(projectsGetCmd)
projectsCmd.AddCommand(projectsUpdateCmd)
projectsCmd.AddCommand(projectsDeleteCmd)
projectsCmd.AddCommand(projectsLimitsCmd)
projectsCmd.AddCommand(projectsGetLimitsCompatCmd)
Expand Down
123 changes: 123 additions & 0 deletions cmd/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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
}

Expand All @@ -45,6 +46,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}, nil
}

func (f *FakeProjectsService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error {
if f.DeleteFunc != nil {
return f.DeleteFunc(ctx, id, opts...)
Expand Down Expand Up @@ -146,6 +154,121 @@ func TestProjectsGet_InvalidOutput(t *testing.T) {
assert.Contains(t, err.Error(), "unsupported --output value")
}

func TestProjectsUpdate_Success(t *testing.T) {
buf := capturePtermOutput(t)
c := ProjectsCmd{
projects: &FakeProjectsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) {
assert.Equal(t, "a12345678901234567890123", id)
assert.True(t, body.UpdateProjectRequest.Name.Valid())
assert.Equal(t, "Renamed", body.UpdateProjectRequest.Name.Value)
return &kernel.Project{ID: id, Name: body.UpdateProjectRequest.Name.Value}, nil
},
},
limits: &FakeProjectLimitsService{},
}

err := c.Update(context.Background(), ProjectsUpdateInput{
Identifier: "a12345678901234567890123",
Name: "Renamed",
})

require.NoError(t, err)
out := buf.String()
assert.Contains(t, out, "Updated project: Renamed")
assert.Contains(t, out, "a12345678901234567890123")
}

func TestProjectsUpdate_JSONOutput(t *testing.T) {
project := mustProject(t, `{"id":"a12345678901234567890123","name":"Renamed","status":"active","created_at":"2026-05-29T12:00:00Z","updated_at":"2026-05-29T12:30:00Z"}`)
c := ProjectsCmd{
projects: &FakeProjectsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) {
assert.Equal(t, "a12345678901234567890123", id)
assert.True(t, body.UpdateProjectRequest.Name.Valid())
assert.Equal(t, "Renamed", body.UpdateProjectRequest.Name.Value)
return &project, nil
},
},
limits: &FakeProjectLimitsService{},
}

var err error
out := captureStdout(t, func() {
err = c.Update(context.Background(), ProjectsUpdateInput{
Identifier: "a12345678901234567890123",
Name: "Renamed",
Output: "json",
})
})

require.NoError(t, err)
assert.JSONEq(t, `{"id":"a12345678901234567890123","name":"Renamed","status":"active","created_at":"2026-05-29T12:00:00Z","updated_at":"2026-05-29T12:30:00Z"}`, out)
}

func TestProjectsUpdate_ResolvesName(t *testing.T) {
c := ProjectsCmd{
projects: &FakeProjectsService{
ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) {
return &pagination.OffsetPagination[kernel.Project]{
Items: []kernel.Project{{ID: "a12345678901234567890123", Name: "Default"}},
}, nil
},
UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) {
assert.Equal(t, "a12345678901234567890123", id)
return &kernel.Project{ID: id, Name: body.UpdateProjectRequest.Name.Value}, nil
},
},
limits: &FakeProjectLimitsService{},
}

err := c.Update(context.Background(), ProjectsUpdateInput{
Identifier: "default",
Name: "Renamed",
})

require.NoError(t, err)
}

func TestProjectsUpdate_RequiresName(t *testing.T) {
c := ProjectsCmd{
projects: &FakeProjectsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) {
t.Fatal("Update should not be called")
return nil, nil
},
},
limits: &FakeProjectLimitsService{},
}

err := c.Update(context.Background(), ProjectsUpdateInput{Identifier: "a12345678901234567890123"})

require.Error(t, err)
assert.Contains(t, err.Error(), "--name is required")
assert.Contains(t, err.Error(), "add --name <name>")
}

func TestProjectsUpdate_InvalidOutput(t *testing.T) {
c := ProjectsCmd{
projects: &FakeProjectsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) {
t.Fatal("Update should not be called")
return nil, nil
},
},
limits: &FakeProjectLimitsService{},
}

err := c.Update(context.Background(), ProjectsUpdateInput{
Identifier: "a12345678901234567890123",
Name: "Renamed",
Output: "yaml",
})

require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported --output value")
}

func TestProjectsLimitsGet_DefaultOutput(t *testing.T) {
buf := capturePtermOutput(t)
limits := &kernel.ProjectLimits{
Expand Down