You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sub-issue of #262. Hardened spec — do not re-derive decisions.
Command Description
List build definition folders under PROJECT, optionally scoped to a sub-path. Mirrors az pipelines folder list.
Locked Decisions
#
Decision
Rationale
1
Project-scoped, with optional path filter.
Folders are owned by projects.
2
Use: "list [ORGANIZATION/]PROJECT".
Mirrors variable-group list's Use: "list [ORGANIZATION/]PROJECT" (internal/cmd/pipelines/variablegroup/list/list.go). The path is an optional--path flag, not a positional.
3
Aliases: []string{"ls", "l"}.
Per AGENTS.md list command convention.
4
cobra.ExactArgs(1).
One positional target: [ORGANIZATION/]PROJECT.
5
Parse the single positional with util.ParseProjectTargetWithDefaultOrganization(ctx, args[0], &opts.org, &opts.project).
The existing helper handles the 2-segment form correctly.
6
--path is an optional string flag. When set, GetFolders is called with Path = &opts.path; when empty, top-level folders are returned.
--query-order is an optional string enum flag accepting asc or desc. Maps to build.FolderQueryOrderValues.FolderAscending / FolderDescending. Use util.StringEnumFlag (or equivalent) for the validation.
Mirrors az pipelines folder list --query-order.
8
Plain output: table with columns PATH, DESCRIPTION.
Mirrors the AzDO Extension's transform_pipelines_folders formatter.
9
JSON output: dedicated view struct folderView with fields path and description. Pass a []folderView (built from the SDK []Folder) to opts.exporter.Write.
The SDK Folder struct exposes many fields (CreatedBy, CreatedOn, LastChangedBy, LastChangedDate, Project), but the AzDO Extension's table output only shows PATH and DESCRIPTION — match that for the list view. The full SDK type is exposed by show-style commands (none here).
10
--max-items is not supported by GetFolders (no continuation token on the response) — but we still accept it for consistency with the rest of the CLI and silently apply it as a post-fetch cap.
Precedent: --max-items in internal/cmd/pipelines/variablegroup/list/list.go.
11
Uses vendored build.Client.GetFolders.
Vendor verified at vendor/.../v7/build/client.go:2490.
Predecessors: the create/delete sub-issues (so the ParseProjectPathTargetWithDefaultOrganization wrapper is available; not strictly required for this issue since list uses a 2-segment target).
14
No go mod tidy / go mod vendor / scripts/generate_mocks.sh work.
Command Signature
varlistCmd=&cobra.Command{
Use: "list [ORGANIZATION/]PROJECT",
Aliases: []string{"ls", "l"},
Short: "List folders.",
Long: `List build definition folders in PROJECT.Mirrors 'az pipelines folder list'. Use --path to limit the listing toa sub-folder. Use --query-order to sort by path ascending or descending.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd*cobra.Command, args []string) error {
returnrunList(cmd.Context(), opts, args[0])
},
}
Flags
Flag
Type
Default
Required
Description
--path
string
""
no
Limit the listing to folders at or under this path.
The vendored SDK enforces args.Project != "" — will return ArgumentNilOrEmptyError if missing. No extra validation needed in our wrapper.
GetFoldersArgs.Path is optional. GetFoldersArgs.QueryOrder is optional. The vendored SDK serializes a nil QueryOrder as an empty query param (no queryOrder added to the URL); the server then returns the default order.
Reference Existing Patterns
internal/cmd/pipelines/variablegroup/list/list.go — primary reference. Mirrors the Use, the Aliases, the view struct pattern, the AddColumns + AddField + EndRow + Render table pattern, and the GetValue/MapSlice helpers.
internal/cmd/repo/list/list.go — secondary reference for the ctx.Printer("list") pattern.
util.StringEnumFlag (or equivalent in internal/cmd/util/flags.go) — for --query-order validation.
Azure DevOps REST API: Folders — Get Folders (GET, route id a906531b-d2da-4f55-bda7-f3e676cc50d9, API version 7.1-preview.2). Path is a route value (not a query param) and is optional.
Sub-issue of #262. Hardened spec — do not re-derive decisions.
Command Description
List build definition folders under
PROJECT, optionally scoped to a sub-path. Mirrorsaz pipelines folder list.Locked Decisions
Use: "list [ORGANIZATION/]PROJECT".variable-group list'sUse: "list [ORGANIZATION/]PROJECT"(internal/cmd/pipelines/variablegroup/list/list.go). The path is an optional--pathflag, not a positional.Aliases: []string{"ls", "l"}.cobra.ExactArgs(1).[ORGANIZATION/]PROJECT.util.ParseProjectTargetWithDefaultOrganization(ctx, args[0], &opts.org, &opts.project).--pathis an optional string flag. When set,GetFoldersis called withPath = &opts.path; when empty, top-level folders are returned.pipeline_folder_list --path <value>(optional).--query-orderis an optional string enum flag acceptingascordesc. Maps tobuild.FolderQueryOrderValues.FolderAscending/FolderDescending. Useutil.StringEnumFlag(or equivalent) for the validation.az pipelines folder list --query-order.PATH,DESCRIPTION.transform_pipelines_foldersformatter.folderViewwith fieldspathanddescription. Pass a[]folderView(built from the SDK[]Folder) toopts.exporter.Write.Folderstruct exposes many fields (CreatedBy, CreatedOn, LastChangedBy, LastChangedDate, Project), but the AzDO Extension's table output only shows PATH and DESCRIPTION — match that for the list view. The full SDK type is exposed byshow-style commands (none here).--max-itemsis not supported byGetFolders(no continuation token on the response) — but we still accept it for consistency with the rest of the CLI and silently apply it as a post-fetch cap.--max-itemsininternal/cmd/pipelines/variablegroup/list/list.go.build.Client.GetFolders.vendor/.../v7/build/client.go:2490.internal/mocks/build_client_mock.go:881-893already mocksGetFolders.create/deletesub-issues (so theParseProjectPathTargetWithDefaultOrganizationwrapper is available; not strictly required for this issue since list uses a 2-segment target).go mod tidy/go mod vendor/scripts/generate_mocks.shwork.Command Signature
Flags
--pathstring""--query-orderstring(enum:asc,desc)""asc→FolderAscending,desc→FolderDescending. Empty → server default.--max-itemsint0(no cap)--organizationstring$AZDO_ORGANIZATIONor default config--projectstring""Also add
util.AddJSONFlags(cmd, &opts.exporter, "path", "description")(only the list view fields are exposed, not the full SDK model).JSON Output Contract
View struct (file-scope, in
list.go):Helper (file-scope):
Build
views := types.MapSlice(*resp, newFolderView)and passviewstoopts.exporter.Write. Apply--max-itemsafter the map if set.Table Output Contract
Use
ctx.Printer("list"):If the result is empty, the printer renders an empty table (no rows). Do not print a synthetic "no folders" message.
Command Wiring
internal/cmd/pipelines/folder/list/list.gowithpackage list, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/folder/list"tointernal/cmd/pipelines/folder/folder.go.folder.gowithcmd.AddCommand(list.NewCmd(ctx)).API Surface
Vendored SDK call (from
vendor/.../v7/build/client.go:2490):The vendored SDK enforces
args.Project != ""— will returnArgumentNilOrEmptyErrorif missing. No extra validation needed in our wrapper.GetFoldersArgs.Pathis optional.GetFoldersArgs.QueryOrderis optional. The vendored SDK serializes a nilQueryOrderas an empty query param (noqueryOrderadded to the URL); the server then returns the default order.Reference Existing Patterns
internal/cmd/pipelines/variablegroup/list/list.go— primary reference. Mirrors theUse, theAliases, the view struct pattern, theAddColumns+AddField+EndRow+Rendertable pattern, and theGetValue/MapSlicehelpers.internal/cmd/repo/list/list.go— secondary reference for thectx.Printer("list")pattern.util.StringEnumFlag(or equivalent ininternal/cmd/util/flags.go) — for--query-ordervalidation.TDD Test Plan
TestNewCmd_listUse == "list [ORGANIZATION/]PROJECT",Aliases == ["ls", "l"],Args == cobra.ExactArgs(1).Test_runList_success_noPathGetFoldersreturns[]build.Folder{{Path: ptr("P/Foo")}, {Path: ptr("P/Bar")}}.P/Foo,P/Bar; DESCRIPTION column is empty.Test_runList_success_withPath--path=Pis set; mock returns 1 folder.args.Path == ptr("P")was passed to the SDK.Test_runList_success_queryOrderAsc--query-order=ascis set.args.QueryOrder == ptr(build.FolderQueryOrderValues.FolderAscending)was passed.Test_runList_success_queryOrderDesc--query-order=descis set.args.QueryOrder == ptr(build.FolderQueryOrderValues.FolderDescending)was passed.Test_runList_invalidQueryOrder--query-order=bananais set.StringEnumFlagreturns a flag-value error. SDK is not called.Test_runList_success_maxItems--max-items=2is set.Test_runList_success_JSON--jsonflag set; mock returns 2 folders.[]folderViewwith 2 entries, each with the rightPathandDescription.Test_runList_empty[]build.Folder{}.Test_runList_APIErrorerrors.New("boom").Test_runList_missingProjectazdo pipelines folder listwith no args.References
vendor/.../v7/build/client.go:2490(GetFolders),:2516(GetFoldersArgs).vendor/.../v7/build/models.go:1401(Folder),:1419(FolderQueryOrder),:1427(FolderQueryOrderValues).internal/mocks/build_client_mock.go:881-893(GetFolders).internal/azdo/factory.go:61—ClientFactory().Build(ctx, organization).a906531b-d2da-4f55-bda7-f3e676cc50d9, API version7.1-preview.2). Path is a route value (not a query param) and is optional.internal/cmd/pipelines/variablegroup/list/list.go(project-scope, list view, table output).