Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ repos:
- id: actionlint-system

- repo: https://github.com/crate-ci/typos
rev: v1.47.2
rev: v1.49.0
hooks:
- id: typos
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ This project relies on the [samber/cc-skills-golang](https://github.com/samber/c

```bash
# Inspect which Go skills are currently installed
npx skills list | grep golang-
skills list | grep golang-

# Install a single skill
npx skills add samber/cc-skills-golang --skill golang-code-style -y
skills add samber/cc-skills-golang --skill golang-code-style -y
```

**Workflow:**

1. Before writing or modifying Go code, run `npx skills list | grep golang-` to see which skills are installed in this project or globally. The agent loads them automatically based on description matching; if `golang-how-to` is installed it also force-loads relevant secondary skills (e.g. Cobra review → `golang-spf13-cobra` + `golang-cli` + `golang-error-handling`).
1. Before writing or modifying Go code, run `skills list | grep golang-` to see which skills are installed in this project or globally. The agent loads them automatically based on description matching; if `golang-how-to` is installed it also force-loads relevant secondary skills (e.g. Cobra review → `golang-spf13-cobra` + `golang-cli` + `golang-error-handling`).
2. Treat the installed samber skills as the source of truth for general Go rules (style, naming, error wrapping, nil safety, testing patterns, concurrency, context propagation, etc.).
3. Apply the project-specific rules in the sections below only where they **deviate** from samber. Every such section declares `> Supersedes samber/cc-skills-golang@<skill> for this project.` at the top — samber's ⚙️ override mechanism is honored automatically.
4. If a section below does not declare a supersession, the samber skills win on that topic.
Expand Down
4 changes: 4 additions & 0 deletions docs/azdo_boards_work-item.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ Work with Azure Boards work items.

### Available commands

* [azdo boards work-item delete](./azdo_boards_work-item_delete.md)
* [azdo boards work-item list](./azdo_boards_work-item_list.md)

### Examples

```bash
# List work items in a project
azdo boards work-item list Fabrikam

# Delete a work item
azdo boards work-item delete Fabrikam/42 --yes
```

### See also
Expand Down
59 changes: 59 additions & 0 deletions docs/azdo_boards_work-item_delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Command `azdo boards work-item delete`

```
azdo boards work-item delete [ORG:]PROJECT/ID [flags]
```

Delete a work item by ID. By default the work item is moved to the
Recycle Bin and can be restored via the Azure DevOps web UI.
Use --destroy to permanently remove the work item; this cannot be
undone.


### Options


* `--destroy`

Permanently delete the work item (bypasses Recycle Bin).

* `-q`, `--jq` `expression`

Filter JSON output using a jq expression

* `--json` `fields`

Output JSON with the specified fields. Prefix a field with &#39;-&#39; to exclude it.

* `-t`, `--template` `string`

Format JSON output using a Go template; see &#34;azdo help formatting&#34;

* `-y`, `--yes`

Skip the confirmation prompt.


### ALIASES

- `d`
- `del`
- `rm`

### JSON Fields

`code`, `deletedBy`, `deletedDate`, `id`, `message`, `name`, `project`, `resource`, `type`, `url`

### Examples

```bash
# Delete a work item in the default organization
azdo boards work-item delete Fabrikam/42 --yes

# Permanently destroy a work item in a specific organization
azdo boards work-item delete myorg:Fabrikam/42 --destroy --yes
```

### See also

* [azdo boards work-item](./azdo_boards_work-item.md)
18 changes: 18 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,24 @@ u, up

Work with Azure Boards work items.

#### `azdo boards work-item delete [ORG:]PROJECT/ID [flags]`

Delete a work item.

```
--destroy Permanently delete the work item (bypasses Recycle Bin).
-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 the confirmation prompt.
```

Aliases

```
d, del, rm
```

#### `azdo boards work-item list [ORG:]PROJECT [flags]`

List work items belonging to a project.
Expand Down
153 changes: 153 additions & 0 deletions internal/cmd/boards/workitem/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package delete

import (
"fmt"
"strconv"

"github.com/MakeNowJust/heredoc/v2"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
"github.com/spf13/cobra"
"go.uber.org/zap"

"github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/shared"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/types"
)

type opts struct {
targetArg string
yes bool
destroy bool
exporter util.Exporter
}

func NewCmd(ctx util.CmdContext) *cobra.Command {
opts := &opts{}

cmd := &cobra.Command{
Use: "delete [ORG:]PROJECT/ID",
Short: "Delete a work item.",
Aliases: []string{"d", "del", "rm"},
Long: heredoc.Doc(`
Delete a work item by ID. By default the work item is moved to the
Recycle Bin and can be restored via the Azure DevOps web UI.
Use --destroy to permanently remove the work item; this cannot be
undone.
`),
Example: heredoc.Doc(`
# Delete a work item in the default organization
azdo boards work-item delete Fabrikam/42 --yes

# Permanently destroy a work item in a specific organization
azdo boards work-item delete myorg:Fabrikam/42 --destroy --yes
`),
Args: util.ExactArgs(1, "project/work item target 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 the confirmation prompt.")
cmd.Flags().BoolVar(&opts.destroy, "destroy", false, "Permanently delete the work item (bypasses Recycle Bin).")
util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "code", "deletedBy", "deletedDate", "message", "name", "project", "type", "url", "resource"})

return cmd
}

func runDelete(cmdCtx util.CmdContext, opts *opts) error {
ios, err := cmdCtx.IOStreams()
if err != nil {
return err
}
ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

scope, err := util.ParseProjectTargetWithDefaultOrganization(cmdCtx, opts.targetArg)
if err != nil {
return util.FlagErrorWrap(err)
}

id, err := strconv.Atoi(scope.Targets[0])
if err != nil || id <= 0 {
return util.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0])
}

zap.L().Debug(
"resolved work item delete target",
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.Int("workItemId", id),
)

client, err := cmdCtx.ClientFactory().WorkItemTracking(cmdCtx.Context(), scope.Organization)
if err != nil {
return fmt.Errorf("failed to create work item tracking client: %w", err)
}

item, err := client.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{
Id: &id,
Project: types.ToPtr(scope.Project),
Fields: types.ToPtr([]string{shared.TeamProjectField}),
})
if err != nil {
return fmt.Errorf("failed to fetch work item %d: %w", id, err)
}
if !shared.BelongsToProject(item, scope.Project) {
return fmt.Errorf("work item %d does not belong to project %q", id, scope.Project)
}

if !opts.yes {
if !ios.CanPrompt() {
return util.FlagErrorf("--yes required when not running interactively")
}
ios.StopProgressIndicator()
prompter, err := cmdCtx.Prompter()
if err != nil {
return err
}
message := "Are you sure you want to delete this work item?"
if opts.destroy {
message = "Are you sure you want to permanently destroy this work item? This cannot be undone."
}
confirmed, err := prompter.Confirm(message, false)
if err != nil {
return err
}
if !confirmed {
zap.L().Debug("work item deletion canceled by user", zap.Int("workItemId", id))
return util.ErrCancel
}
ios.StartProgressIndicator()
}

res, err := client.DeleteWorkItem(cmdCtx.Context(), workitemtracking.DeleteWorkItemArgs{
Project: types.ToPtr(scope.Project),
Id: &id,
Destroy: &opts.destroy,
})
if err != nil {
return fmt.Errorf("failed to delete work item %d: %w", id, err)
}

zap.L().Debug(
"work item deleted",
zap.Int("workItemId", id),
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.Bool("destroy", opts.destroy),
)

ios.StopProgressIndicator()

if opts.exporter != nil {
return opts.exporter.Write(ios, res)
}

if opts.destroy {
fmt.Fprintf(ios.Out, "Permanently deleted work item %d\n", id)
return nil
}
fmt.Fprintf(ios.Out, "Deleted work item %d\n", id)
return nil
}
Loading
Loading