From b25f09358a71f382c827388d4783827bc9653751 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Sat, 6 Jun 2026 16:50:19 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(team):=20=E2=9C=A8=20add=20command=20t?= =?UTF-8?q?o=20delete=20teams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a dedicated subcommand under the team group, supporting GUID or name targets, optional non-interactive mode, progress feedback, and structured output export. --- internal/cmd/team/delete/delete.go | 109 +++++++++++++++++++++++++++++ internal/cmd/team/team.go | 2 + 2 files changed, 111 insertions(+) create mode 100644 internal/cmd/team/delete/delete.go diff --git a/internal/cmd/team/delete/delete.go b/internal/cmd/team/delete/delete.go new file mode 100644 index 0000000..9b77f78 --- /dev/null +++ b/internal/cmd/team/delete/delete.go @@ -0,0 +1,109 @@ +package delete + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" + "github.com/spf13/cobra" + "github.com/tmeckel/azdo-cli/internal/cmd/util" +) + +type deleteOptions struct { + targetArg string + yes bool + exporter util.Exporter +} + +func NewCmd(ctx util.CmdContext) *cobra.Command { + opts := &deleteOptions{} + + cmd := &cobra.Command{ + Use: "delete [ORGANIZATION/]PROJECT/TEAM", + Short: "Delete a team.", + Long: heredoc.Doc(` + Delete a team from a project. + + The TEAM argument accepts the ID (GUID) or name of the team. + A confirmation prompt is shown unless --yes is provided. + `), + Example: heredoc.Doc(` + # Delete a team (with confirmation) + azdo team delete Fabrikam/"Old Team" + + # Delete a team without confirmation + azdo team delete MyOrg/Fabrikam/00000002-0000-0000-0000-000000000000 --yes + `), + Aliases: []string{"d", "del", "rm"}, + Args: util.ExactArgs(1, "team argument required"), + RunE: func(cmd *cobra.Command, args []string) error { + opts.targetArg = args[0] + return runDelete(ctx, opts) + }, + } + + cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, "Skip confirmation prompt") + + util.AddJSONFlags(cmd, &opts.exporter, []string{}) + + return cmd +} + +func runDelete(ctx util.CmdContext, opts *deleteOptions) error { + ios, err := ctx.IOStreams() + if err != nil { + return err + } + + cs := ios.ColorScheme() + + p, err := ctx.Prompter() + if err != nil { + return err + } + + scope, err := util.ParseProjectTargetWithDefaultOrganization(ctx, opts.targetArg) + if err != nil { + return util.FlagErrorWrap(err) + } + + teamID := scope.Targets[0] + + if !opts.yes { + confirmed, err := p.Confirm(fmt.Sprintf("Delete team %s from project %s?", + cs.Bold(teamID), cs.Bold(fmt.Sprintf("%s/%s", scope.Organization, scope.Project))), false) + if err != nil { + return err + } + if !confirmed { + return util.ErrCancel + } + } + + ios.StartProgressIndicator() + defer ios.StopProgressIndicator() + + client, err := ctx.ClientFactory().Core(ctx.Context(), scope.Organization) + if err != nil { + return fmt.Errorf("failed to create Core client: %w", err) + } + + if err := client.DeleteTeam(ctx.Context(), core.DeleteTeamArgs{ + ProjectId: &scope.Project, + TeamId: &teamID, + }); err != nil { + return fmt.Errorf("failed to delete team: %w", err) + } + + ios.StopProgressIndicator() + + if opts.exporter != nil { + return opts.exporter.Write(ios, struct{}{}) + } + + if ios.IsStdoutTTY() { + fmt.Fprintf(ios.Out, "%s Team %s deleted successfully.\n", cs.SuccessIcon(), cs.Bold(teamID)) + } + + return nil +} diff --git a/internal/cmd/team/team.go b/internal/cmd/team/team.go index 9150bd3..98d84a1 100644 --- a/internal/cmd/team/team.go +++ b/internal/cmd/team/team.go @@ -3,6 +3,7 @@ package team import ( "github.com/spf13/cobra" "github.com/tmeckel/azdo-cli/internal/cmd/team/create" + "github.com/tmeckel/azdo-cli/internal/cmd/team/delete" "github.com/tmeckel/azdo-cli/internal/cmd/team/list" "github.com/tmeckel/azdo-cli/internal/cmd/team/show" "github.com/tmeckel/azdo-cli/internal/cmd/team/update" @@ -20,6 +21,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command { } cmd.AddCommand(create.NewCmd(ctx)) + cmd.AddCommand(delete.NewCmd(ctx)) cmd.AddCommand(list.NewCmd(ctx)) cmd.AddCommand(show.NewCmd(ctx)) cmd.AddCommand(update.NewCmd(ctx)) From cedbf11ccf6824ac09b34da6a6ecddac4803005a Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Sat, 6 Jun 2026 16:51:28 +0000 Subject: [PATCH 2/3] =?UTF-8?q?test(team):=20=F0=9F=A7=AA=20add=20tests=20?= =?UTF-8?q?for=20team=20delete=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cmd/team/delete/delete_test.go | 217 ++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 internal/cmd/team/delete/delete_test.go diff --git a/internal/cmd/team/delete/delete_test.go b/internal/cmd/team/delete/delete_test.go new file mode 100644 index 0000000..d99482b --- /dev/null +++ b/internal/cmd/team/delete/delete_test.go @@ -0,0 +1,217 @@ +package delete + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tmeckel/azdo-cli/internal/cmd/util" + "github.com/tmeckel/azdo-cli/internal/iostreams" + "github.com/tmeckel/azdo-cli/internal/mocks" + "go.uber.org/mock/gomock" +) + +type fakeDeleteDeps struct { + cmd *mocks.MockCmdContext + clientFact *mocks.MockClientFactory + core *mocks.MockCoreClient + prompter *mocks.MockPrompter + stdout *bytes.Buffer +} + +func setupFakeDeleteDeps(t *testing.T, organization string) *fakeDeleteDeps { + t.Helper() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, out, _ := iostreams.Test() + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + + deps := &fakeDeleteDeps{ + cmd: mocks.NewMockCmdContext(ctrl), + clientFact: mocks.NewMockClientFactory(ctrl), + core: mocks.NewMockCoreClient(ctrl), + prompter: mocks.NewMockPrompter(ctrl), + stdout: out, + } + + deps.cmd.EXPECT().IOStreams().Return(io, nil).AnyTimes() + deps.cmd.EXPECT().Context().Return(context.Background()).AnyTimes() + deps.cmd.EXPECT().ClientFactory().Return(deps.clientFact).AnyTimes() + deps.cmd.EXPECT().Prompter().Return(deps.prompter, nil).AnyTimes() + deps.clientFact.EXPECT().Core(gomock.Any(), organization).Return(deps.core, nil).AnyTimes() + + return deps +} + +func TestDelete_MissingTeamArg(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCmdCtx := mocks.NewMockCmdContext(ctrl) + + cmd := NewCmd(mockCmdCtx) + cmd.SetArgs([]string{}) + err := cmd.Execute() + assert.Error(t, err) + assert.Contains(t, err.Error(), "team argument required") +} + +func TestDelete_RequiresConfirmationByDefault(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.prompter.EXPECT().Confirm(gomock.Any(), false).Return(false, nil) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/MyTeam"}) + err := cmd.Execute() + assert.ErrorIs(t, err, util.ErrCancel) +} + +func TestDelete_YesFlagSkipsConfirmation(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.core.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()).Return(nil) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/MyTeam", "--yes"}) + err := cmd.Execute() + require.NoError(t, err) + assert.Empty(t, deps.stdout.String()) +} + +func TestDelete_ConfirmationAccept_InvokesDeleteTeam(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.prompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + deps.core.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, args core.DeleteTeamArgs) error { + require.NotNil(t, args.ProjectId) + require.NotNil(t, args.TeamId) + assert.Equal(t, "myProject", *args.ProjectId) + assert.Equal(t, "My Team", *args.TeamId) + return nil + }) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/My Team"}) + err := cmd.Execute() + require.NoError(t, err) +} + +func TestDelete_TargetArg_ParsesOrgSlashProjectSlashTeam(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.prompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + deps.core.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, args core.DeleteTeamArgs) error { + require.NotNil(t, args.ProjectId) + assert.Equal(t, "myProject", *args.ProjectId) + require.NotNil(t, args.TeamId) + assert.Equal(t, "My Team", *args.TeamId) + return nil + }) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/My Team"}) + err := cmd.Execute() + require.NoError(t, err) +} + +func TestDelete_DefaultsToConfiguredOrganization(t *testing.T) { + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, out, _ := iostreams.Test() + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + + mockCmdCtx := mocks.NewMockCmdContext(ctrl) + mockClientFactory := mocks.NewMockClientFactory(ctrl) + mockCoreClient := mocks.NewMockCoreClient(ctrl) + mockConfig := mocks.NewMockConfig(ctrl) + mockAuthCfg := mocks.NewMockAuthConfig(ctrl) + mockPrompter := mocks.NewMockPrompter(ctrl) + + defaultOrg := "defaultOrg" + + mockCmdCtx.EXPECT().Config().Return(mockConfig, nil).AnyTimes() + mockConfig.EXPECT().Authentication().Return(mockAuthCfg).AnyTimes() + mockAuthCfg.EXPECT().GetDefaultOrganization().Return(defaultOrg, nil).AnyTimes() + mockCmdCtx.EXPECT().IOStreams().Return(io, nil).AnyTimes() + mockCmdCtx.EXPECT().Context().Return(context.Background()).AnyTimes() + mockCmdCtx.EXPECT().ClientFactory().Return(mockClientFactory).AnyTimes() + mockCmdCtx.EXPECT().Prompter().Return(mockPrompter, nil).AnyTimes() + mockClientFactory.EXPECT().Core(gomock.Any(), defaultOrg).Return(mockCoreClient, nil).AnyTimes() + mockCoreClient.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()).Return(nil) + mockPrompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + + cmd := NewCmd(mockCmdCtx) + cmd.SetArgs([]string{"myProject/MyTeam"}) + err := cmd.Execute() + require.NoError(t, err) + assert.Empty(t, out.String()) +} + +func TestDelete_TTYOutput(t *testing.T) { + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, out, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStderrTTY(false) + + mockCmdCtx := mocks.NewMockCmdContext(ctrl) + mockClientFactory := mocks.NewMockClientFactory(ctrl) + mockCoreClient := mocks.NewMockCoreClient(ctrl) + mockPrompter := mocks.NewMockPrompter(ctrl) + + mockCmdCtx.EXPECT().IOStreams().Return(io, nil).AnyTimes() + mockCmdCtx.EXPECT().Context().Return(context.Background()).AnyTimes() + mockCmdCtx.EXPECT().ClientFactory().Return(mockClientFactory).AnyTimes() + mockCmdCtx.EXPECT().Prompter().Return(mockPrompter, nil).AnyTimes() + mockClientFactory.EXPECT().Core(gomock.Any(), "myOrg").Return(mockCoreClient, nil).AnyTimes() + mockCoreClient.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()).Return(nil) + mockPrompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + + cmd := NewCmd(mockCmdCtx) + cmd.SetArgs([]string{"myOrg/myProject/MyTeam"}) + err := cmd.Execute() + require.NoError(t, err) + + output := out.String() + assert.Contains(t, output, "deleted successfully") + assert.Contains(t, output, "MyTeam") +} + +func TestDelete_NonTTYOutput(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.prompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + deps.core.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()).Return(nil) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/MyTeam"}) + err := cmd.Execute() + require.NoError(t, err) + assert.Empty(t, deps.stdout.String()) +} + +func TestDelete_PropagatesSDKError(t *testing.T) { + deps := setupFakeDeleteDeps(t, "myOrg") + + deps.prompter.EXPECT().Confirm(gomock.Any(), false).Return(true, nil) + deps.core.EXPECT().DeleteTeam(gomock.Any(), gomock.Any()).Return(errors.New("API error")) + + cmd := NewCmd(deps.cmd) + cmd.SetArgs([]string{"myOrg/myProject/MyTeam"}) + err := cmd.Execute() + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to delete team: API error") +} From 1299205a204fea759d765ba53abd1b627ce322c5 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Sat, 6 Jun 2026 16:52:26 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(team):=20=F0=9F=93=84add=20documentati?= =?UTF-8?q?on=20for=20team=20delete=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/azdo_help_reference.md | 17 +++++++++++++ docs/azdo_team.md | 1 + docs/azdo_team_delete.md | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 docs/azdo_team_delete.md diff --git a/docs/azdo_help_reference.md b/docs/azdo_help_reference.md index c96c03a..9fa4479 100644 --- a/docs/azdo_help_reference.md +++ b/docs/azdo_help_reference.md @@ -1321,6 +1321,23 @@ Aliases c, cr, new, n, add, a ``` +### `azdo team delete [ORGANIZATION/]PROJECT/TEAM [flags]` + +Delete a team. + +``` +-q, --jq expression Filter JSON output using a jq expression + --json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it. +-t, --template string Format JSON output using a Go template; see "azdo help formatting" +-y, --yes Skip confirmation prompt +``` + +Aliases + +``` +d, del, rm +``` + ### `azdo team list [ORGANIZATION/]PROJECT [flags]` List teams in a project. diff --git a/docs/azdo_team.md b/docs/azdo_team.md index 7f1b0d4..ebf3bea 100644 --- a/docs/azdo_team.md +++ b/docs/azdo_team.md @@ -5,6 +5,7 @@ Manage Azure DevOps teams. ### Available commands * [azdo team create](./azdo_team_create.md) +* [azdo team delete](./azdo_team_delete.md) * [azdo team list](./azdo_team_list.md) * [azdo team show](./azdo_team_show.md) * [azdo team update](./azdo_team_update.md) diff --git a/docs/azdo_team_delete.md b/docs/azdo_team_delete.md new file mode 100644 index 0000000..91a6c33 --- /dev/null +++ b/docs/azdo_team_delete.md @@ -0,0 +1,51 @@ +## Command `azdo team delete` + +``` +azdo team delete [ORGANIZATION/]PROJECT/TEAM [flags] +``` + +Delete a team from a project. + +The TEAM argument accepts the ID (GUID) or name of the team. +A confirmation prompt is shown unless --yes is provided. + + +### Options + + +* `-q`, `--jq` `expression` + + Filter JSON output using a jq expression + +* `--json` `fields` + + Output JSON with the specified fields. Prefix a field with '-' to exclude it. + +* `-t`, `--template` `string` + + Format JSON output using a Go template; see "azdo help formatting" + +* `-y`, `--yes` + + Skip confirmation prompt + + +### ALIASES + +- `d` +- `del` +- `rm` + +### Examples + +```bash +# Delete a team (with confirmation) +azdo team delete Fabrikam/"Old Team" + +# Delete a team without confirmation +azdo team delete MyOrg/Fabrikam/00000002-0000-0000-0000-000000000000 --yes +``` + +### See also + +* [azdo team](./azdo_team.md)