Sub-issue of new boards work-item relation umbrella. Hardened spec — do not re-derive decisions. Sibling of #271 (add), #273 (remove), #274 (show).
Command Description
List the relation (link) types supported in the Azure DevOps organization, together with each type's referenceName and attributes (e.g., enabled, usage, isDirectory). Mirrors az boards work-item relation list-type.
The REST surface is Relation Types - Get (REST 7.1):
GET https://dev.azure.com/{organization}/_apis/wit/workitemrelationtypes?api-version=7.1-preview.2
The Python reference implementation is at azure-devops/azext_devops/dev/boards/relations.py:16-19 (get_relation_types_show function) and the table transformer is at azure-devops/azext_devops/dev/boards/_format.py:7-19 (transform_work_item_relation_type_table_output).
The Python's table transformer renders 4 columns: Name, ReferenceName, Enabled, Usage. The attributes object can have additional fields like isDirectory and resourceSubType; we surface only the documented four to keep the table narrow, but include all of attributes in the JSON output.
Locked Decisions
| # |
Decision |
Rationale |
| 1 |
Org-scoped. Use: "list-type [ORGANIZATION]". No positional ID — this is a list of static metadata, not tied to a work item. |
Mirrors az boards work-item relation list-type. |
| 2 |
Aliases: []string{"lt", "list-types"}. |
Per AGENTS.md list command convention (the lt short form is preferred for clarity here). |
| 3 |
cobra.MaximumNArgs(1). Accepts 0 or 1 positional: an optional explicit organization. |
Mirrors az boards work-item relation list-type (no required ID). |
| 4 |
Parse the optional positional with util.ParseTargetWithDefaultOrganization(ctx, args[0]) when 1 arg is given; otherwise resolve the default org from cmdCtx. |
Existing helper handles 1- and 2-segment forms; 0 args means use the default org. |
| 5 |
No --project flag. The endpoint is org-scoped. |
Matches az boards work-item relation list-type (no project flag). |
| 6 |
Uses vendored workitemtracking.Client.GetRelationTypes. |
Vendor verified at vendor/.../v7/workitemtracking/client.go:113 (interface), :1854 (impl). |
| 7 |
JSON output uses a dedicated view struct with omitempty pointer fields. The struct flattens the Attributes map into Enabled, Usage, IsDirectory, ResourceSubType (each *string with omitempty). |
Mirrors #249 (variablegroup list) conventions. List commands use a view struct, not the raw SDK type. |
| 8 |
Default output: table with columns NAME, REFERENCE NAME, ENABLED, USAGE (4 columns, matching the Python's transform_work_item_relation_type_table_output). |
Mirrors Python. |
| 9 |
Stable alphabetical sort by Name (case-insensitive) in the default table view. JSON output is unsorted. |
Determinism. The Python's transform_work_item_relation_type_table_output does not sort, but azdo table output is sorted by the first column for stability. |
| 10 |
--max-items flag with the standard semantics from internal/cmd/util/max_items.go (introduced by #249). Caps the row count after sort. |
Mirrors variablegroup/list/list.go. |
| 11 |
--json, --jq, --template via util.AddJSONFlags(cmd, &opts.exporter, []string{"name", "referenceName", "enabled", "usage", "isDirectory", "resourceSubType", "attributes", "url", "_links"}). |
Every JSON field of the view struct. |
| 12 |
No pagination. GetRelationTypes returns the full list (no continuation token). |
SDK confirmed at client.go:1854 — no ContinuationToken in the response. |
| 13 |
No new mocks needed. |
internal/mocks/workitemtracking_client_mock.go:669 already mocks GetRelationTypes. |
| 14 |
No go mod tidy / go mod vendor / scripts/generate_mocks.sh work. |
|
| 15 |
No progress indicator for trivial list operations — match the pattern from variablegroup/list/list.go. |
Decision is consistent with sibling list commands. |
Command Signature
var listTypeCmd = &cobra.Command{
Use: "list-type [ORGANIZATION]",
Aliases: []string{"lt", "list-types"},
Short: "List work item relation types.",
Long: heredoc.Doc(`
List the relation (link) types supported in the Azure DevOps
organization. Each entry includes a friendly name and its
reference name; use the friendly name with 'add' and 'remove'.
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.orgArg = ""
if len(args) == 1 {
opts.orgArg = args[0]
}
return runListType(cmd.Context(), opts)
},
}
Flags
| Flag |
Notes |
--organization |
Default org from config. |
--max-items |
Caps row count after sort. |
Also add util.AddJSONFlags(cmd, &opts.exporter, []string{"name", "referenceName", "enabled", "usage", "isDirectory", "resourceSubType", "attributes", "url", "_links"}).
Code Skeleton (canonical, copy verbatim)
§1. listTypeOptions struct and relationTypeView view struct
type listTypeOptions struct {
orgArg string
maxItems int
exporter util.Exporter
}
type relationTypeView struct {
Name *string `json:"name,omitempty"`
ReferenceName *string `json:"referenceName,omitempty"`
Enabled *string `json:"enabled,omitempty"`
Usage *string `json:"usage,omitempty"`
IsDirectory *string `json:"isDirectory,omitempty"`
ResourceSubType *string `json:"resourceSubType,omitempty"`
Attributes *map[string]any `json:"attributes,omitempty"`
Url *string `json:"url,omitempty"`
Links any `json:"_links,omitempty"`
}
§2. View mapping helper
func mapRelationType(rt *workitemtracking.WorkItemRelationType) relationTypeView {
v := relationTypeView{
Name: rt.Name,
ReferenceName: rt.ReferenceName,
Url: rt.Url,
Links: rt.Links,
}
if rt.Attributes != nil {
if a, ok := (*rt.Attributes)["enabled"]; ok { v.Enabled = attrString(a) }
if a, ok := (*rt.Attributes)["usage"]; ok { v.Usage = attrString(a) }
if a, ok := (*rt.Attributes)["isDirectory"]; ok { v.IsDirectory = attrString(a) }
if a, ok := (*rt.Attributes)["resourceSubType"]; ok { v.ResourceSubType = attrString(a) }
if a, ok := (*rt.Attributes)["resourceSubType"]; ok { v.ResourceSubType = attrString(a) }
// Always include the full attributes map for parity with the JSON contract.
attrs := make(map[string]any, len(*rt.Attributes))
for k, v := range *rt.Attributes { attrs[k] = v }
v.Attributes = &attrs
}
return v
}
func attrString(a any) *string {
if b, ok := a.(bool); ok {
return types.ToPtr(strconv.FormatBool(b))
}
if s, ok := a.(string); ok {
return &s
}
if i, ok := a.(int); ok {
return types.ToPtr(strconv.Itoa(i))
}
return nil
}
§3. runListType skeleton
func runListType(cmdCtx util.CmdContext, opts *listTypeOptions) error {
ios, err := cmdCtx.IOStreams()
if err != nil { return err }
ios.StartProgressIndicator()
defer ios.StopProgressIndicator()
org := ""
if opts.orgArg != "" {
scope, err := util.ParseTargetWithDefaultOrganization(cmdCtx, opts.orgArg)
if err != nil { return util.FlagErrorWrap(err) }
org = scope.Organization
} else {
org = cmdCtx.Config().DefaultOrganization()
}
wit, err := cmdCtx.ClientFactory().WorkItemTracking(cmdCtx.Context(), org)
if err != nil { return err }
types, err := wit.GetRelationTypes(cmdCtx.Context(), workitemtracking.GetRelationTypesArgs{})
if err != nil { return err }
// Stable sort by Name (case-insensitive).
sort.SliceStable(*types, func(i, j int) bool {
return strings.ToLower(types.GetValue((*types)[i].Name, "")) <
strings.ToLower(types.GetValue((*types)[j].Name, ""))
})
if opts.maxItems > 0 && len(*types) > opts.maxItems {
truncated := (*types)[:opts.maxItems]
types = &truncated
}
if opts.exporter != nil {
views := types.MapSlicePtr(*types, mapRelationType) // types.MapSlicePtr is the project's generic slice mapper
return opts.exporter.Write(ios, views)
}
tp, err := cmdCtx.Printer("list")
if err != nil { return err }
tp.AddColumns("NAME", "REFERENCE NAME", "ENABLED", "USAGE")
for _, rt := range *types {
tp.AddField(types.GetValue(rt.Name, ""))
tp.AddField(types.GetValue(rt.ReferenceName, ""))
enabled := ""
if rt.Attributes != nil {
if a, ok := (*rt.Attributes)["enabled"]; ok { enabled = types.GetValue(attrString(a), "") }
}
tp.AddField(enabled)
usage := ""
if rt.Attributes != nil {
if a, ok := (*rt.Attributes)["usage"]; ok { usage = types.GetValue(attrString(a), "") }
}
tp.AddField(usage)
tp.EndRow()
}
return tp.Render()
}
JSON Output Contract
Pass a []relationTypeView (one entry per relation type, in the unsorted order returned by the SDK) to opts.exporter.Write. The view struct uses omitempty so absent attributes do not appear in the payload.
JSON fields exposed: name, referenceName, enabled, usage, isDirectory, resourceSubType, attributes, url, _links.
Table Output Contract
Use ctx.Printer("list") with 4 columns: NAME, REFERENCE NAME, ENABLED, USAGE. Rows are sorted by Name (case-insensitive) and capped at --max-items if set.
Command Wiring
- Create
internal/cmd/boards/workitem/relation/listtype/listtype.go with package listtype, factory func NewCmd(ctx util.CmdContext) *cobra.Command.
- Add
import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/listtype" to internal/cmd/boards/workitem/relation/relation.go.
- Register in
relation.go with cmd.AddCommand(listtype.NewCmd(ctx)).
- The directory name is
listtype (single word) to match the Use: "list-type" form (Go package directories cannot contain hyphens).
API Surface
Vendored SDK call (from vendor/.../v7/workitemtracking/client.go:1854):
types, err := wit.GetRelationTypes(ctx, workitemtracking.GetRelationTypesArgs{})
The GetRelationTypesArgs struct is empty (no Project, no ContinuationToken). The endpoint is org-scoped.
Reference Existing Patterns
internal/cmd/pipelines/variablegroup/list/list.go — primary list reference (mirrors --max-items, ctx.Printer("list"), view struct, stable sort).
internal/cmd/boards/workitem/list/list.go — sibling work-item list (sibling reference, table shape).
Reference implementations (for UX parity only — azdo is the implementation target):
- AzDO Extension
azure-devops/azext_devops/dev/boards/relations.py:16-19 - get_relation_types_show function. Confirms: org-scoped, returns the full list.
- AzDO Extension
azure-devops/azext_devops/dev/boards/_format.py:7-19 - transform_work_item_relation_type_table_output confirms table columns Name, ReferenceName, Enabled, Usage.
- AzDO MCP Server (TypeScript): no dedicated relation-type tool. The static set of relation types is documented inline in the server's prompt context.
TDD Test Plan
| Test name |
Scenario |
Expected |
TestNewCmd_listType |
Inspect the command struct. |
Use == "list-type [ORGANIZATION]", Aliases == ["lt", "list-types"], Args == cobra.MaximumNArgs(1). |
Test_runListType_defaultOrg |
No positional. |
Mock GetRelationTypes called with empty org from config. |
Test_runListType_explicitOrg |
Positional is "myorg". |
Mock GetRelationTypes called with "myorg". |
Test_runListType_twoArgRejected |
Two positionals. |
Cobra returns the standard "accepts at most 1 arg" error. |
Test_runListType_emptyOrg |
Positional is "" (zero args). |
Default org from config used. |
Test_runListType_basic |
Mock returns 3 relation types in input order [c, a, b]. |
Table rows rendered in order [a, b, c] (sorted by Name). |
Test_runListType_maxItems |
Mock returns 10 types; --max-items 3 set. |
Table has 3 rows: [a, b, c]. |
Test_runListType_maxItemsZeroOrNegative |
--max-items 0 or --max-items -1. |
All rows are returned (no cap when <= 0). |
Test_runListType_JSON |
--json flag set. |
Exporter receives []relationTypeView with 3 entries; referenceName is the referenceName from SDK; attributes map is preserved. |
Test_runListType_JSON_omitsAbsentAttributes |
Mock returns a type with only enabled in attributes (no usage). |
The corresponding view has enabled set; usage, isDirectory, resourceSubType are absent (omitempty). |
Test_runListType_tableEnabledBool |
attributes["enabled"] = true (bool). |
Table ENABLED cell renders "true". |
Test_runListType_tableEnabledFalse |
attributes["enabled"] = false. |
Table ENABLED cell renders "false". |
Test_runListType_tableUsageString |
attributes["usage"] = "workItemLink". |
Table USAGE cell renders "workItemLink". |
Test_runListType_APIError |
Mock returns errors.New("boom"). |
Returns wrapped error. |
Test_runListType_nilAttributes |
Attributes is nil on a type. |
ENABLED and USAGE cells render as empty string; no panic. |
References
Sub-issue of new
boards work-item relationumbrella. Hardened spec — do not re-derive decisions. Sibling of #271 (add), #273 (remove), #274 (show).Command Description
List the relation (link) types supported in the Azure DevOps organization, together with each type's
referenceNameandattributes(e.g.,enabled,usage,isDirectory). Mirrorsaz boards work-item relation list-type.The REST surface is Relation Types - Get (REST 7.1):
The Python reference implementation is at
azure-devops/azext_devops/dev/boards/relations.py:16-19(get_relation_types_showfunction) and the table transformer is atazure-devops/azext_devops/dev/boards/_format.py:7-19(transform_work_item_relation_type_table_output).The Python's table transformer renders 4 columns:
Name,ReferenceName,Enabled,Usage. Theattributesobject can have additional fields likeisDirectoryandresourceSubType; we surface only the documented four to keep the table narrow, but include all ofattributesin the JSON output.Locked Decisions
Use: "list-type [ORGANIZATION]". No positional ID — this is a list of static metadata, not tied to a work item.az boards work-item relation list-type.Aliases: []string{"lt", "list-types"}.ltshort form is preferred for clarity here).cobra.MaximumNArgs(1). Accepts 0 or 1 positional: an optional explicit organization.az boards work-item relation list-type(no required ID).util.ParseTargetWithDefaultOrganization(ctx, args[0])when 1 arg is given; otherwise resolve the default org fromcmdCtx.--projectflag. The endpoint is org-scoped.az boards work-item relation list-type(no project flag).workitemtracking.Client.GetRelationTypes.vendor/.../v7/workitemtracking/client.go:113(interface),:1854(impl).omitemptypointer fields. The struct flattens theAttributesmap intoEnabled,Usage,IsDirectory,ResourceSubType(each*stringwithomitempty).NAME, REFERENCE NAME, ENABLED, USAGE(4 columns, matching the Python'stransform_work_item_relation_type_table_output).Name(case-insensitive) in the default table view. JSON output is unsorted.transform_work_item_relation_type_table_outputdoes not sort, butazdotable output is sorted by the first column for stability.--max-itemsflag with the standard semantics frominternal/cmd/util/max_items.go(introduced by #249). Caps the row count after sort.variablegroup/list/list.go.--json,--jq,--templateviautil.AddJSONFlags(cmd, &opts.exporter, []string{"name", "referenceName", "enabled", "usage", "isDirectory", "resourceSubType", "attributes", "url", "_links"}).GetRelationTypesreturns the full list (no continuation token).client.go:1854— noContinuationTokenin the response.internal/mocks/workitemtracking_client_mock.go:669already mocksGetRelationTypes.go mod tidy/go mod vendor/scripts/generate_mocks.shwork.variablegroup/list/list.go.Command Signature
Flags
--organization--max-itemsAlso add
util.AddJSONFlags(cmd, &opts.exporter, []string{"name", "referenceName", "enabled", "usage", "isDirectory", "resourceSubType", "attributes", "url", "_links"}).Code Skeleton (canonical, copy verbatim)
§1.
listTypeOptionsstruct andrelationTypeViewview struct§2. View mapping helper
§3.
runListTypeskeletonJSON Output Contract
Pass a
[]relationTypeView(one entry per relation type, in the unsorted order returned by the SDK) toopts.exporter.Write. The view struct usesomitemptyso absent attributes do not appear in the payload.JSON fields exposed:
name,referenceName,enabled,usage,isDirectory,resourceSubType,attributes,url,_links.Table Output Contract
Use
ctx.Printer("list")with 4 columns:NAME, REFERENCE NAME, ENABLED, USAGE. Rows are sorted byName(case-insensitive) and capped at--max-itemsif set.Command Wiring
internal/cmd/boards/workitem/relation/listtype/listtype.gowithpackage listtype, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/listtype"tointernal/cmd/boards/workitem/relation/relation.go.relation.gowithcmd.AddCommand(listtype.NewCmd(ctx)).listtype(single word) to match theUse: "list-type"form (Go package directories cannot contain hyphens).API Surface
Vendored SDK call (from
vendor/.../v7/workitemtracking/client.go:1854):The
GetRelationTypesArgsstruct is empty (noProject, noContinuationToken). The endpoint is org-scoped.Reference Existing Patterns
internal/cmd/pipelines/variablegroup/list/list.go— primary list reference (mirrors--max-items,ctx.Printer("list"), view struct, stable sort).internal/cmd/boards/workitem/list/list.go— sibling work-item list (sibling reference, table shape).Reference implementations (for UX parity only —
azdois the implementation target):azure-devops/azext_devops/dev/boards/relations.py:16-19-get_relation_types_showfunction. Confirms: org-scoped, returns the full list.azure-devops/azext_devops/dev/boards/_format.py:7-19-transform_work_item_relation_type_table_outputconfirms table columnsName, ReferenceName, Enabled, Usage.TDD Test Plan
TestNewCmd_listTypeUse == "list-type [ORGANIZATION]",Aliases == ["lt", "list-types"],Args == cobra.MaximumNArgs(1).Test_runListType_defaultOrgGetRelationTypescalled with empty org from config.Test_runListType_explicitOrg"myorg".GetRelationTypescalled with"myorg".Test_runListType_twoArgRejectedTest_runListType_emptyOrg""(zero args).Test_runListType_basic[c, a, b].[a, b, c](sorted by Name).Test_runListType_maxItems--max-items 3set.[a, b, c].Test_runListType_maxItemsZeroOrNegative--max-items 0or--max-items -1.<= 0).Test_runListType_JSON--jsonflag set.[]relationTypeViewwith 3 entries;referenceNameis thereferenceNamefrom SDK;attributesmap is preserved.Test_runListType_JSON_omitsAbsentAttributesenabledinattributes(nousage).enabledset;usage,isDirectory,resourceSubTypeare absent (omitempty).Test_runListType_tableEnabledBoolattributes["enabled"] = true(bool).ENABLEDcell renders"true".Test_runListType_tableEnabledFalseattributes["enabled"] = false.ENABLEDcell renders"false".Test_runListType_tableUsageStringattributes["usage"] = "workItemLink".USAGEcell renders"workItemLink".Test_runListType_APIErrorerrors.New("boom").Test_runListType_nilAttributesAttributesisnilon a type.ENABLEDandUSAGEcells render as empty string; no panic.References
vendor/.../v7/workitemtracking/client.go:113 (GetRelationTypes), :1854 (impl).vendor/.../v7/workitemtracking/models.go:1435 (WorkItemRelationType).internal/mocks/workitemtracking_client_mock.go:669 (GetRelationTypes).internal/azdo/factory.go:149—ClientFactory().WorkItemTracking(ctx, organization).7.1-preview.2).boards work-item relationsubgroup. Sibling: Introduceazdo boards work-item relationcommand group #271, Implementazdo boards work-item relation list-typecommand #273, Implementazdo boards work-item relation removecommand #274.internal/cmd/pipelines/variablegroup/list/list.go(feat: Implementazdo pipelines queue listcommand #249).