Skip to content

Commit b836a8f

Browse files
author
Joan-Angelo Enrile
committed
feat: add team management commands and enhance work item filtering
1 parent a08b5f6 commit b836a8f

10 files changed

Lines changed: 317 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ src/
1717
api/
1818
client.ts # WebApi factory; credential priority: SYSTEM_ACCESSTOKEN > AZURE_DEVOPS_TOKEN > stored
1919
workItems.ts # Work item CRUD, comments, branch linking
20+
teamSettings.ts # Team listing and iteration (sprint) resolution
2021
pullRequests.ts # PR listing, diff, review, threads
2122
builds.ts # Pipeline run list/view/cancel/rerun/download/delete
2223
search.ts # Code (Azure Search REST), commits, repos (client-side filter)
@@ -30,6 +31,7 @@ src/
3031
run/ # list, view, watch, cancel, rerun, download, delete
3132
search/ # issues, code, commits, prs, repos, projects
3233
repo/ # list, clone
34+
team/ # list, iteration list
3335
completion.ts # Shell completions (bash/zsh/fish/powershell)
3436
config/index.ts # Config load: CLI > env > git remote > ~/.ado/config.json
3537
output/index.ts # TTY/non-TTY formatting, table, detail, JSON

plugins/azure-devops/skills/azure-devops/SKILL.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Available on every command:
9595

9696
| Command | Description |
9797
|---------|-------------|
98-
| `ado issue list` | List work items (`-s`, `-a`, `-l`, `-t`, `--limit`) |
98+
| `ado issue list` | List work items (`-s`, `-a`, `-l`, `-i`, `-t`, `--limit`) |
9999
| `ado issue view <id>` | Work item details (`--comments`) |
100100
| `ado issue create` | Create work item (`-t` required, `--type`, `-b`, `-a`, `-l`) |
101101
| `ado issue edit <id>` | Update fields (`-t`, `-b`, `-s`, `-a`, `-l`) |
@@ -107,9 +107,19 @@ Available on every command:
107107

108108
### Common patterns
109109

110+
**State filter** accepts boolean expressions: `!Closed & !Resolved`, `Active | In Progress`, `Active,In Progress`. Default (`!removed & !deleted & !closed`) excludes terminal states.
111+
112+
**Iteration filter** (`-i`) defaults to `current` sprint. Use `next`, `all`, or a literal path like `MyProject\Sprint 5`.
113+
110114
```bash
111-
# List open bugs assigned to yourself
112-
ado issue list --state open --type Bug --assignee @me
115+
# List open bugs assigned to yourself in the current sprint
116+
ado issue list --type Bug --assignee @me
117+
118+
# List all active items across all sprints
119+
ado issue list --state Active --iteration all
120+
121+
# List items in the next sprint
122+
ado issue list --iteration next
113123
114124
# Create a bug
115125
ado issue create --title "Login fails on Safari" --type Bug --assignee user@org.com
@@ -192,6 +202,27 @@ ado search code "AuthController" --repo backend-api --limit 10
192202
ado search prs "hotfix"
193203
```
194204

205+
## Teams & Iterations
206+
207+
| Command | Description |
208+
|---------|-------------|
209+
| `ado team list` | List teams in a project (`--mine`, `--limit`) |
210+
| `ado team iteration list <team>` | List iterations (sprints) for a team (`--current`) |
211+
212+
```bash
213+
# List all teams
214+
ado team list
215+
216+
# List only teams you belong to
217+
ado team list --mine
218+
219+
# List all iterations for a team
220+
ado team iteration list "My Team"
221+
222+
# Show just the current sprint
223+
ado team iteration list "My Team" --current --json
224+
```
225+
195226
## Repositories
196227

197228
```bash

plugins/azure-devops/skills/azure-devops/reference.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ Generated from source at `src/commands/**/*.ts`. Update this file whenever a com
2828

2929
| Flag | Short | Description | Default |
3030
|------|-------|-------------|---------|
31-
| `--state <state>` | `-s` | Filter: `open\|closed\|all` | `open` |
31+
| `--state <expr>` | `-s` | State filter expression (see below) | `!removed & !deleted & !closed` |
3232
| `--assignee <who>` | `-a` | Filter by assignee; use `@me` for yourself ||
3333
| `--label <tag>` | `-l` | Filter by tag/label ||
34+
| `--iteration <path>` | `-i` | Iteration path, or `current`, `next`, `all` | `current` |
3435
| `--type <type>` | `-t` | Work item type (e.g. `Bug`, `Task`, `User Story`) ||
3536
| `--limit <n>` || Max items to return | `30` |
3637
| `--project <project>` | `-p` | Azure DevOps project (overrides config) ||
3738
| `--org <url>` || Organization URL (overrides config) ||
3839
| `--json [fields]` || Output as JSON ||
3940
| `--web` | `-w` | Open in browser ||
4041

42+
**State expression syntax:** comma or `|` separates OR clauses; `&` separates AND terms; `!` negates a state. Examples: `Active`, `Active,In Progress`, `!Closed & !Resolved`, `Active | In Progress`.
43+
4144
## ado issue view \<id\>
4245

4346
| Flag | Short | Description | Default |
@@ -323,6 +326,25 @@ Generated from source at `src/commands/**/*.ts`. Update this file whenever a com
323326
| `--project <project>` | `-p` | Azure DevOps project ||
324327
| `--org <url>` || Organization URL ||
325328

329+
## ado team list
330+
331+
| Flag | Short | Description | Default |
332+
|------|-------|-------------|---------|
333+
| `--mine` || Only teams you are a member of ||
334+
| `--limit <n>` || Max results | `30` |
335+
| `--project <project>` | `-p` | Azure DevOps project (overrides config) ||
336+
| `--org <url>` || Organization URL (overrides config) ||
337+
| `--json [fields]` || Output as JSON ||
338+
339+
## ado team iteration list \<team\>
340+
341+
| Flag | Short | Description | Default |
342+
|------|-------|-------------|---------|
343+
| `--current` || Show only the current iteration ||
344+
| `--project <project>` | `-p` | Azure DevOps project (overrides config) ||
345+
| `--org <url>` || Organization URL (overrides config) ||
346+
| `--json [fields]` || Output as JSON ||
347+
326348
## ado completion \<shell\>
327349

328350
Accepted values for `<shell>`: `bash`, `zsh`, `fish`, `powershell`

src/api/teamSettings.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as azdev from 'azure-devops-node-api';
2+
import { WebApiTeam } from 'azure-devops-node-api/interfaces/CoreInterfaces';
3+
import { TeamSettingsIteration } from 'azure-devops-node-api/interfaces/WorkInterfaces';
4+
5+
6+
/**
7+
* Get a list of teams.
8+
*/
9+
export async function getTeams(
10+
connection: azdev.WebApi,
11+
project: string,
12+
options: {
13+
/**
14+
* If true return all the teams requesting user is member, otherwise return all the teams user has read access.
15+
*/
16+
mine?: boolean;
17+
skip?: number;
18+
top?: number;
19+
/**
20+
* A value indicating whether or not to expand Identity information in the result WebApiTeam object.
21+
*/
22+
expandIdentity?: boolean;
23+
}
24+
): Promise<WebApiTeam[]> {
25+
const coreApi = await connection.getCoreApi();
26+
const teams = await coreApi.getTeams(project, options.mine, options.top, options.skip, options.expandIdentity);
27+
return teams;
28+
}
29+
30+
/**
31+
* Get a team's iterations using timeframe filter.
32+
*/
33+
export async function getTeamIterations(
34+
connection: azdev.WebApi,
35+
project: string,
36+
team: string,
37+
options: {
38+
current?: boolean;
39+
}
40+
): Promise<TeamSettingsIteration[]> {
41+
const workApi = await connection.getWorkApi();
42+
43+
const timeframe = options.current ? 'Current' : undefined;
44+
45+
const iterations = await workApi.getTeamIterations({project, team}, timeframe);
46+
return iterations;
47+
}

src/api/workItems.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export interface WorkItemDetail extends WorkItemSummary {
107107
priority: string;
108108
areaPath: string;
109109
iterationPath: string;
110+
acceptanceCriteria: string;
110111
raw: Record<string, unknown>;
111112
}
112113

@@ -130,20 +131,32 @@ export async function listWorkItems(
130131
tag?: string;
131132
limit?: number;
132133
type?: string;
134+
iterationPath?: string;
133135
}
134136
): Promise<WorkItemSummary[]> {
135137
const witApi = await connection.getWorkItemTrackingApi();
136138

137139
const conditions: string[] = [`[System.TeamProject] = '${project.replace(/'/g, "''")}'`];
138140

139-
const state = options.state ?? 'open';
140-
if (state === 'open') {
141-
conditions.push(`[System.State] <> 'Closed'`);
142-
conditions.push(`[System.State] <> 'Resolved'`);
143-
} else if (state === 'closed') {
144-
conditions.push(`([System.State] = 'Closed' OR [System.State] = 'Resolved')`);
141+
const orStates = options.state
142+
?.split(/,|\||\|\|/)
143+
.map(s => s
144+
.split(/&|&&/)
145+
.map(t => t.trim())
146+
.map(s => ({
147+
not: s.startsWith('!'),
148+
state: s.replace(/^!/g, "").trim(),
149+
})));
150+
151+
if (orStates && orStates.length > 0) {
152+
const statesCondition = orStates
153+
.map(andStates => andStates
154+
.map(s => `[System.State] ${s.not ? '<>' : '='} '${s.state.replace(/'/g, "''")}'`)
155+
.join(' AND ')
156+
).join(' OR ');
157+
158+
conditions.push(`(${statesCondition})`);
145159
}
146-
// 'all' → no state filter
147160

148161
if (options.assignee) {
149162
if (options.assignee === '@me') {
@@ -161,6 +174,10 @@ export async function listWorkItems(
161174
conditions.push(`[System.WorkItemType] = '${options.type.replace(/'/g, "''")}'`);
162175
}
163176

177+
if (options.iterationPath) {
178+
conditions.push(`[System.IterationPath] CONTAINS '${options.iterationPath.replace(/'/g, "''")}'`);
179+
}
180+
164181
const wiql = {
165182
query: `SELECT [System.Id] FROM WorkItems WHERE ${conditions.join(' AND ')} ORDER BY [System.ChangedDate] DESC`,
166183
};
@@ -228,6 +245,7 @@ export async function getWorkItem(
228245
priority: String(f['Microsoft.VSTS.Common.Priority'] ?? ''),
229246
areaPath: String(f['System.AreaPath'] ?? ''),
230247
iterationPath: String(f['System.IterationPath'] ?? ''),
248+
acceptanceCriteria: String(f['Microsoft.VSTS.Common.AcceptanceCriteria'] ?? ''),
231249
url: buildOrgUrl(orgUrl, project, wi.id ?? id),
232250
raw: f as Record<string, unknown>,
233251
};

src/commands/issue/list.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,65 @@ import { getWebApi } from '../../api/client.js';
33
import { listWorkItems } from '../../api/workItems.js';
44
import { getConfig } from '../../config/index.js';
55
import { outputTable, outputJson, relativeDate, colorState, truncate } from '../../output/index.js';
6+
import { getTeamIterations, getTeams } from '../../api/teamSettings.js';
7+
import { WebApi } from 'azure-devops-node-api';
8+
9+
10+
async function getIterationPathByOptions(connection: WebApi, project: string, iteration: string | undefined): Promise<string | undefined> {
11+
if (!iteration) {
12+
return undefined;
13+
}
14+
15+
iteration = iteration.toLowerCase();
16+
17+
if (iteration === 'all') {
18+
return undefined;
19+
}
20+
21+
if (iteration !== 'current' && iteration !== 'next') {
22+
return iteration;
23+
}
24+
25+
26+
const myTeams = await getTeams(connection, project, { mine: true });
27+
28+
if (myTeams.length === 0) {
29+
process.stdout.write('No teams found for the current user.\n');
30+
return undefined;
31+
}
32+
33+
const team = myTeams[0].name!;
34+
const iterations = await getTeamIterations(connection, project, team, { current: true });
35+
36+
if (iterations.length === 0) {
37+
process.stdout.write(`No current iteration found for team ${team}.\n`);
38+
return undefined;
39+
}
40+
41+
const currentIteration = iterations[0];
42+
if (iteration === 'current') {
43+
return currentIteration.path;
44+
}
45+
46+
if (iteration === 'next') {
47+
const allIterations = await getTeamIterations(connection, project, team, {});
48+
const currentIndex = allIterations.findIndex(i => i.id === currentIteration.id);
49+
50+
if (currentIndex === -1 || currentIndex === allIterations.length - 1) {
51+
process.stdout.write(`No next iteration found for team ${team}.\n`);
52+
return undefined;
53+
}
54+
55+
return allIterations[currentIndex + 1].path;
56+
}
57+
58+
return undefined;
59+
}
660

761
async function issueListHandler(options: {
862
state?: string;
963
assignee?: string;
64+
iteration?: string;
1065
label?: string;
1166
limit?: string;
1267
project?: string;
@@ -26,12 +81,15 @@ async function issueListHandler(options: {
2681

2782
const connection = await getWebApi(config.orgUrl);
2883

84+
let iterationPath = await getIterationPathByOptions(connection, config.project, options.iteration);
85+
2986
const items = await listWorkItems(connection, config.project, {
3087
state: options.state,
3188
assignee: options.assignee,
3289
tag: options.label,
3390
limit: options.limit ? parseInt(options.limit, 10) : 30,
3491
type: options.type,
92+
iterationPath,
3593
});
3694

3795
if (options.json !== undefined) {
@@ -71,9 +129,10 @@ export function registerIssueList(issueCmd: Command): void {
71129
issueCmd
72130
.command('list')
73131
.description('List work items in a project')
74-
.option('-s, --state <state>', 'Filter by state: open|closed|all', 'open')
132+
.option('-s, --state <state>', 'Filter by states (comma-separated, e.g. open,closed)', '!removed & !deleted & !closed')
75133
.option('-a, --assignee <assignee>', 'Filter by assignee (use @me for yourself)')
76134
.option('-l, --label <label>', 'Filter by tag/label')
135+
.option('-i, --iteration <path>', 'Filter by iteration path (or current, next, all)', 'current')
77136
.option('-t, --type <type>', 'Filter by work item type (e.g. Bug, Task, User Story)')
78137
.option('--limit <number>', 'Max items to return', '30')
79138
.option('-p, --project <project>', 'Azure DevOps project (overrides config)')

src/commands/team/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Command } from 'commander';
2+
import { registerTeamList } from './list.js';
3+
import { registerTeamIteration } from './iteration.js';
4+
5+
export function registerTeamCommands(program: Command): void {
6+
const team = program.command('team').description('Manage Azure DevOps teams');
7+
registerTeamList(team);
8+
registerTeamIteration(team);
9+
}

0 commit comments

Comments
 (0)