ByteAid.Tools.Ado.Cli 1.0.0-alpha.2

This is a prerelease version of ByteAid.Tools.Ado.Cli.
dotnet tool install --global ByteAid.Tools.Ado.Cli --version 1.0.0-alpha.2
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local ByteAid.Tools.Ado.Cli --version 1.0.0-alpha.2
                    
This package contains a .NET tool you can call from the shell/command line.
#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/devicelogin URL + 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.Msal to 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 --json payload); all diagnostics go to stderr, so a --json payload is always clean to pipe.
  • Exit 0 on success; exit 2 on 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 exits 0.

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 tag x exactly, 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 --upsert immediately 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.CommandLine command tree on the generic host (Host.CreateDefaultBuilder + UseHost). Verb groups (show/create/update/delete) are populated per area; the work items area is wired by AddWorkItemsCommands. Input augmentation runs in Program.cs before InvokeAsync.
  • Core (ByteAid.Tools.Ado) — AzureDevOpsTokenProvider (auth + cache, registered as a singleton via an async factory) and per-area services that depend only on IAzureDevOpsTokenProvider. Work items split read (WorkItemsService) and write (WorkItemWriteService) paths; mutations are sent as application/json-patch+json.
  • Adding an area — add a service in the core library taking IAzureDevOpsTokenProvider, register it in ServiceCollectionExtensions, 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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