ByteAid.Tools.Ado.Cli
1.0.0-alpha.2
dotnet tool install --global ByteAid.Tools.Ado.Cli --version 1.0.0-alpha.2
dotnet new tool-manifest
dotnet tool install --local ByteAid.Tools.Ado.Cli --version 1.0.0-alpha.2
#tool dotnet:?package=ByteAid.Tools.Ado.Cli&version=1.0.0-alpha.2&prerelease
nuke :add-package ByteAid.Tools.Ado.Cli --version 1.0.0-alpha.2
ByteAid.Tools.Ado
bta-ado is a general-purpose CLI for Azure DevOps. It authenticates with a work/school
account (Entra ID) via the device code flow — no PATs — and caches tokens in the OS-native
encrypted store so subsequent runs renew silently.
The surface is organized into command areas, one per Azure DevOps domain. Work items is the first area implemented; boards, repos, pipelines, and artifacts are the intended trajectory. Each new area plugs into the same hosting, authentication, and input-resolution pipeline described below.
Packages
| Package | Contents |
|---|---|
ByteAid.Tools.Ado.Cli |
The bta-ado global tool (command tree, hosting, input resolution). |
ByteAid.Tools.Ado |
Reusable core library: token provider + per-area services. Consumable independently of the CLI. |
Both inherit a single <Version> from Directory.Build.props. Target framework: .NET 10.
Installation
dotnet tool install --global ByteAid.Tools.Ado.Cli # install
dotnet tool update --global ByteAid.Tools.Ado.Cli # update
Requires the .NET 10 SDK. After install, bta-ado is on the PATH.
Authentication
Auth is centralized in AzureDevOpsTokenProvider (core library) and shared across every area.
- Flow: Entra ID device code against a multi-tenant ByteAid app registration. First run prints
a
https://microsoft.com/deviceloginURL + code; after sign-in the access token (~1h) and refresh token are cached. - Resource: tokens are minted for the Azure DevOps first-party resource
(
499b84ac-1321-427f-aa17-267ca6975798/.default), constant across tenants/orgs. - Cache: persisted via
Microsoft.Identity.Client.Extensions.Msalto the OS-native encrypted store —%APPDATA%/b8/adocli(Windows DPAPI), Keychain (macOS), Keyring (Linux). The MSAL cache is multi-account: it can hold tokens for N signed-in accounts simultaneously. Silent acquisition reuses the refresh token; only an expired/absent refresh token falls back to device code. - Account selection: with several accounts cached,
--account <username>picks which one to use. Omitted, the first cached account is used. No cached match triggers a fresh device-code sign-in. - Tenant targeting:
--tenant-id <GUID>directs the token request at the tenant backing the target org. Omitted, the request goes to the caller's home tenant via/organizations.
App registration prerequisite (one-time). The app must have "Allow public client flows" enabled in Entra ID, otherwise the device code request is rejected.
Headless / WSL2. If no secure store is available, the provider falls back to an in-memory cache (re-auth each run) and logs a warning instead of failing.
Command structure
bta-ado
└── <verb> <area> [options] verbs: show | create | update | delete
Work items area:
bta-ado show workitems List work items in a project (WIQL + batch fetch; type/state/tag/where filters)
bta-ado show workitem Show one item by id with its relations (parent + children)
bta-ado show workitemtypes List the project's valid types; infer Agile vs Scrum
bta-ado create workitem Create a single work item, optionally parented and tagged
bta-ado create tree Create or idempotently --upsert a work item hierarchy from a JSON spec
bta-ado update workitem Patch fields, re-parent, merge tags on an existing work item
bta-ado delete workitem Delete work item(s), optionally recursive/destructive
Root options (all commands): --version, -h|--help.
Options common to every work items subcommand:
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Azure DevOps organization name |
--project |
-p |
yes | Project name |
--tenant-id |
no | Tenant backing the org (omit for your home tenant) | |
--account |
no | Account username to use when several are signed in (omit = first cached) | |
--json |
no | Emit the stable machine-readable JSON contract on stdout instead of the human view |
All commands hit the Azure DevOps REST API at api-version 7.1.
Output & exit codes
- stdout carries only data (the table or the
--jsonpayload); all diagnostics go to stderr, so a--jsonpayload is always clean to pipe. - Exit
0on success; exit2on any gate-worthy failure (auth, API error, bad input, type mismatch). An idempotent no-op (re-applying the same parent, removing an absent parent) still exits0.
The --json contract
Stable, camelCase, indented; every key is always present (nulls emitted). A single work item:
{
"id": 1234, "type": "Bug", "state": "Active", "title": "Login fails on Safari",
"tags": ["bx:BG-009", "frontend"],
"fields": { "Microsoft.VSTS.Common.Priority": "1" },
"relations": {
"parent": { "id": 1200, "rel": "System.LinkTypes.Hierarchy-Reverse" },
"children": [ { "id": 1250, "rel": "System.LinkTypes.Hierarchy-Forward" } ]
}
}
tags is System.Tags split into entries; fields carries only the --field reference names requested (identity fields collapse to display name); relations.parent is null for an orphan. show workitems wraps the list as { project, count, workItems: [...] }; show workitemtypes returns { process, project, types: [...] }; create tree returns { project, count, items: [{ key, id, type, title, parentId, action }] } with action ∈ created|updated|unchanged.
Commands
show workitems
Runs a WIQL query (SELECT [System.Id] FROM WorkItems ...), then batch-fetches fields (200 ids per
request) and renders a table.
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--type |
-t |
no | Filter by type (Bug, "User Story", Task, …) |
--state |
-s |
no | Filter by state (Active, Closed, …) |
--tag |
no | Filter by an exact tag via WIQL CONTAINS (repeatable, AND-combined) |
|
--where |
no | Raw WIQL boolean clause appended to the WHERE with AND | |
--field |
no | Extra field reference name surfaced in JSON output (repeatable) | |
--json |
no | Emit JSON instead of the table | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in | |
--top |
no | Max work items to return |
bta-ado show workitems -o myOrg -p myProject
bta-ado show workitems -o myOrg -p myProject --type "User Story" --state Active --top 50
bta-ado show workitems -o myOrg -p myProject --tag "bx:FT-001" \
--field Microsoft.VSTS.Common.Priority --json
bta-ado show workitems -o myOrg -p myProject --where "[Microsoft.VSTS.Common.Priority] = 1"
WIQL tag matching is whole-tag, not substring —
[System.Tags] CONTAINS 'x'matches the tagxexactly, and WIQL has no tag prefix/wildcard.--tag "bx:"matches nothing; use the full--tag "bx:FT-001". To pull many correlated items in one query, OR the exact tags via--where([System.Tags] CONTAINS 'bx:FT-001' OR …) or keep a constant marker tag on every managed item.
show workitem
Fetches one item by id with its relations ($expand=relations). --relations renders the
parent/children tree in the human view; relations are always present in --json. Use it to detect
orphans (relations.parent == null) or verify hierarchy.
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--id |
yes | Work item id to show | |
--relations |
no | Render the parent/children tree (always in --json) |
|
--field |
no | Extra field reference name surfaced in JSON (repeatable) | |
--json |
no | Emit JSON instead of the human view | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado show workitem -o myOrg -p myProject --id 1234 --relations
bta-ado show workitem -o myOrg -p myProject --id 1234 --json
show workitemtypes
Lists the work item types the project's process defines and infers the process family (Agile,
Scrum, CMMI, Basic, Unknown) — useful before authoring a tree spec.
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--json |
no | Emit JSON ({ process, project, types }) |
|
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado show workitemtypes -o myOrg -p myProject
bta-ado show workitemtypes -o myOrg -p myProject --json
create workitem
Creates a single work item via a JSON Patch document, optionally linking it under a parent
(System.LinkTypes.Hierarchy-Reverse).
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--type |
-t |
yes | Type (Epic, Feature, "User Story", Task, …) |
--title |
yes | Work item title | |
--description |
-d |
no | Description (System.Description) |
--parent |
no | Parent work item id to link beneath | |
--tag |
no | Tag to stamp on the new item (repeatable; e.g. bx:FT-001) |
|
--json |
no | Emit the created item as JSON | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado create workitem -o myOrg -p myProject -t Feature --title "Digital check-in" --tag bx:FT-001
bta-ado create workitem -o myOrg -p myProject -t Task --title "Validate document" \
-d "Scan and validate the ID document" --parent 1234 --json
Prints the assigned id: Created Feature #1234: Digital check-in.
create tree
Creates a whole hierarchy from a JSON spec; each node is created and linked under its parent recursively. Referenced types are validated against the project's process up front, so a process mismatch (e.g. Scrum vs Agile) fails with one clear error instead of a partial tree.
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--file |
-f |
yes | Path to the JSON tree spec |
--upsert |
no | Idempotent: match nodes by their <prefix><key> correlation tag and update in place instead of duplicating |
|
--tag-prefix |
no | Prefix for a node's key correlation tag (default bx:) |
|
--json |
no | Emit the per-node result list as JSON | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado create tree -o myOrg -p myProject -f ./samples/guest-app-tree.json
bta-ado create tree -o myOrg -p myProject -f ./backlog.json --upsert --json
Spec format. The root requires type and title; description, key, and children are
optional and nest recursively. Comments and trailing commas are tolerated. See
samples/guest-app-tree.json.
A node's optional key is its stable correlation id. On create it is stamped as the tag
<prefix><key> (e.g. bx:FT-001) so the item is correlatable on the next push. With --upsert, a
node whose correlation tag already exists in the project is updated in place (title, description,
re-parented if drifted) instead of duplicated — making the push idempotent.
⚠️ Upsert matching reads ADO's WIQL tag index, which lags a few seconds behind writes. Re-running
--upsertimmediately after a create may not see the just-created items and can still duplicate them. Leave a short gap between successive pushes.
{
"type": "Epic", "title": "Guest mobile app", "key": "EP-001",
"children": [
{
"type": "Feature", "title": "Digital check-in and mobile key", "key": "FT-001",
"children": [
{
"type": "User Story", "title": "As a guest I want to check in from the app", "key": "US-001",
"children": [
{ "type": "Task", "title": "Data capture form", "key": "TK-001" },
{ "type": "Task", "title": "ID document validation", "key": "TK-002" }
]
}
]
}
]
}
update workitem
Patches fields on an existing work item, and manages its parent link and tags. Omitted options leave
state untouched. Sending zero changes is an error — but an idempotent op that resolves to a no-op
(re-applying the same parent, removing an absent parent) succeeds (exit 0).
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--id |
yes | Work item id to edit | |
--title |
no | New title | |
--description |
-d |
no | New description |
--state |
-s |
no | New state (Active, Closed, …) |
--field |
no | Raw field update Reference.Name=value (repeatable) |
|
--parent |
no | Re-parent under this id (idempotent; replaces any existing parent) | |
--remove-parent |
no | Drop the parent link (mutually exclusive with --parent) |
|
--add-tag |
no | Tag to merge into System.Tags without clobbering (repeatable) |
|
--remove-tag |
no | Tag to remove from System.Tags (repeatable) |
|
--json |
no | Emit the updated item as JSON | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado update workitem -o myOrg -p myProject --id 1234 --state Active
bta-ado update workitem -o myOrg -p myProject --id 9 --parent 1234 # re-parent an orphan Bug under its Feature
bta-ado update workitem -o myOrg -p myProject --id 1234 --add-tag bx:FT-001 --add-tag frontend
bta-ado update workitem -o myOrg -p myProject --id 1234 \
--title "New title" \
--field System.AssignedTo=user@org.com \
--field Microsoft.VSTS.Common.Priority=1
--field takes the Azure DevOps reference name and value separated by =, and is repeatable to
set multiple fields in one call. --parent re-parents via System.LinkTypes.Hierarchy-Reverse
(removing any prior parent first); --add-tag/--remove-tag merge into the existing tag set,
whereas --field System.Tags="a; b" replaces it.
delete workitem
Deletes one or more work items. Default sends them to the recycle bin (recoverable). --recursive
expands each id into a post-order delete (children before parents); --destroy permanently removes.
| Option | Alias | Required | Description |
|---|---|---|---|
--organization |
-o |
yes | Organization |
--project |
-p |
yes | Project |
--id |
yes | Id(s) to delete (accepts multiple) | |
--recursive |
-r |
no | Also delete all descendants (children first) |
--destroy |
no | Permanently destroy instead of recycle bin (IRREVERSIBLE) | |
--json |
no | Emit the deleted id list as JSON | |
--tenant-id |
no | Org tenant | |
--account |
no | Account to use when several are signed in |
bta-ado delete workitem -o myOrg -p myProject --id 1234
bta-ado delete workitem -o myOrg -p myProject --id 1234 5678 9012
bta-ado delete workitem -o myOrg -p myProject --id 1234 --recursive
bta-ado delete workitem -o myOrg -p myProject --id 1234 --destroy # cannot be undone
Input resolution
Any option not provided on the command line is resolved from two fallback sources, in descending precedence:
command line > BTA_ADO_ARGS_* (environment) > .bta/ado.json (directory)
Resolution is centralized in EnvironmentInputs.AugmentArgs, which augments the parsed args before
invocation. --help/--version short-circuit augmentation.
Environment variables (BTA_ADO_ARGS_*)
Any option maps to BTA_ADO_ARGS_{OPTION} (dashes → underscores, case-insensitive). The command line
always wins over the environment.
export BTA_ADO_ARGS_ORGANIZATION=myOrg
export BTA_ADO_ARGS_PROJECT=myProject
export BTA_ADO_ARGS_TENANT_ID=00000000-0000-0000-0000-000000000000
bta-ado show workitems
Directory memory (.bta/ado.json)
To pin a repo's context (org, project, tenant, which account) without exporting variables or repeating
flags, drop a .bta/ado.json. Discovery walks up from the current directory (like git locating
.git): the nearest file wins and inherits keys it doesn't set from ancestors. .bta/ is the ByteAid
umbrella folder — each tool reads its own .bta/<tool>.json sibling under the same discovery rule.
// <repo>/.bta/ado.json
{
"organization": "myOrg",
"project": "myProject",
"tenant-id": "00000000-0000-0000-0000-000000000000",
"account": "user@contoso.com" // which cached account to use
}
Keys are the option long names (organization, tenant-id, account, …). A malformed or unreadable
file is ignored without aborting the command.
appsettings.json / User Secrets
The host also reads appsettings.json, user secrets (local dev), and environment variables. Keys
under WorkItems:
{
"WorkItems": {
"DefaultOrganization": null,
"DefaultProject": null,
"TenantId": null,
"Account": null,
"MaxResults": 200
},
"Debug": false
}
Set "Debug": true (or the BTA_ADO_ARGS_* equivalent) for verbose logging.
Architecture
- CLI (
ByteAid.Tools.Ado.Cli) —System.CommandLinecommand tree on the generic host (Host.CreateDefaultBuilder+UseHost). Verb groups (show/create/update/delete) are populated per area; the work items area is wired byAddWorkItemsCommands. Input augmentation runs inProgram.csbeforeInvokeAsync. - Core (
ByteAid.Tools.Ado) —AzureDevOpsTokenProvider(auth + cache, registered as a singleton via an async factory) and per-area services that depend only onIAzureDevOpsTokenProvider. Work items split read (WorkItemsService) and write (WorkItemWriteService) paths; mutations are sent asapplication/json-patch+json. - Adding an area — add a service in the core library taking
IAzureDevOpsTokenProvider, register it inServiceCollectionExtensions, and attach its commands to the existing verb groups. Auth, input resolution, and logging are inherited unchanged.
Versioning & publishing
Version is single-sourced in Directory.Build.props (<Version>); both projects inherit it. The
ci-cd.yaml pipeline packs and publishes to NuGet.org on every push to master (--skip-duplicate,
idempotent). To release: bump <Version>, commit, push.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-alpha.2 | 92 | 6/16/2026 |
| 1.0.0-alpha.1 | 78 | 6/15/2026 |