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.
Project Scope Requirement
PROJECT is mandatory for this command, even though relation types are organization metadata and the SDK endpoint has no project parameter.
- Use [ORG:]PROJECT. If ORG is omitted, resolve the configured default organization. Missing PROJECT, missing organization, and legacy slash forms such as ORG/PROJECT are errors.
- Parse and validate the full project scope before constructing the organization client; use scope.Organization for GetRelationTypes and retain scope.Project as the required CLI scope.
Locked Decisions
| # |
Decision |
Rationale |
| 1 |
Project scope required. Use: "list-type [ORG:]PROJECT". PROJECT is required even though this command lists organization metadata and accepts no work-item ID. |
User requirement for consistent project scoping. |
| 1a |
Parse and validate the project scope; use scope.Organization for the organization-scoped SDK call and retain scope.Project as required CLI scope. |
Keeps command grammar consistent without inventing an SDK project parameter. |
| 2 |
Aliases: []string{"lt", "list-types"}. |
Per AGENTS.md list command convention (the lt short form is preferred for clarity here). |
| 3 |
util.ExactArgs(1, "project argument required"). |
Project scope is required. |
| 3a |
Legacy slash forms and missing PROJECT are rejected. |
Scope must be explicit. |
| 4 |
Parse the required positional with util.ParseProjectScope(ctx, args[0]). |
ORG may be omitted only when configured default organization exists; PROJECT may not be omitted. |
| 5 |
No --project flag; PROJECT is the required positional scope. The endpoint remains organization-scoped, so only scope.Organization is sent to the SDK. |
Project is still mandatory at CLI boundary. |
| 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 [ORG:]PROJECT",
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: util.ExactArgs(1, "project argument required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.scopeArg = 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()
scope, err := util.ParseProjectScope(cmdCtx, opts.scopeArg)
if err != nil { return util.FlagErrorWrap(err) }
org := scope.Organization
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 [ORG:]PROJECT", Aliases == ["lt", "list-types"], Args == util.ExactArgs(1, "project argument required"). |
Test_runListType_projectScopeDefaultOrg |
Positional is "Fabrikam". |
Mock GetRelationTypes called with empty org from config. |
Test_runListType_explicitOrganizationProject |
Positional is "myorg". |
Mock GetRelationTypes called with "myorg". |
Test_runListType_twoArgRejected |
Two positionals. |
Cobra returns the standard "accepts exactly 1 arg" error. |
Test_runListType_missingProject |
Positional is "" (zero args). |
Returns the required project argument error. |
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.Project Scope Requirement
PROJECT is mandatory for this command, even though relation types are organization metadata and the SDK endpoint has no project parameter.
Locked Decisions
Use: "list-type [ORG:]PROJECT". PROJECT is required even though this command lists organization metadata and accepts no work-item ID.Aliases: []string{"lt", "list-types"}.ltshort form is preferred for clarity here).util.ExactArgs(1, "project argument required").util.ParseProjectScope(ctx, args[0]).--projectflag; PROJECT is the required positional scope. The endpoint remains organization-scoped, so only scope.Organization is sent to the SDK.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 [ORG:]PROJECT",Aliases == ["lt", "list-types"],Args == util.ExactArgs(1, "project argument required").Test_runListType_projectScopeDefaultOrgGetRelationTypescalled with empty org from config.Test_runListType_explicitOrganizationProject"myorg".GetRelationTypescalled with"myorg".Test_runListType_twoArgRejectedTest_runListType_missingProject""(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).