azdo-cli 0.22.0-develop.787 → 0.22.0-develop.793

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +129 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@ Azure DevOps CLI focused on work item read/write workflows.
15
15
  - Attach a local file to a work item, or remove a named attachment (`add-attachment`, `delete-attachment`)
16
16
  - Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
17
17
  - Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
18
- - Check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), update an existing PR's title or description (`pr update`), abandon or reactivate one (`pr abandon` / `pr reactivate`), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
18
+ - List pull requests with their draft state, dates, reviewers, labels and linked work item ids (`pr list --work-items`), check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), update an existing PR's title or description (`pr update`), abandon or reactivate one (`pr abandon` / `pr reactivate`), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
19
19
  - Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
20
20
  - Persist org/project/default fields in local config (`config`)
21
21
  - List all fields of a work item (`list-fields`)
@@ -67,9 +67,11 @@ azdo delete-attachment 12345 screenshot.png --yes # skip the prompt (scr
67
67
  azdo delete-attachment 12345 screenshot.png --id <guid> # disambiguate when the name is shared
68
68
 
69
69
  # Find a pull request — one API call, any branch
70
- azdo pr list # active PRs in the repository
71
- azdo pr list --branch feature/x --json # id, title, source/target, author, url, description
70
+ azdo pr list # active PRs in the repository ([active, draft] marks a draft)
71
+ azdo pr list --branch feature/x --json # id, title, source/target, author, url, description, isDraft,
72
+ # creationDate, closedDate, reviewers (uniqueName, vote), labels
72
73
  azdo pr list --status all --top 50
74
+ azdo pr list --work-items --json # + workItemIds per PR: the whole PR ↔ work item map in one call
73
75
 
74
76
  # PR comment threads — list, filter, target by number, resolve or reopen
75
77
  azdo pr comments # active-branch PR; code-anchored threads show file:line
@@ -121,6 +123,8 @@ azdo pr work-items unlink 1234 --pr-number 64
121
123
  azdo pr reviewers add jane@example.com --pr-number 64 # optional by default
122
124
  azdo pr reviewers add jane@example.com --pr-number 64 --required # required (or promotes in place)
123
125
  azdo pr reviewers remove jane@example.com --pr-number 64
126
+ azdo pr reviewers list --pr-number 64 # who must review, and how each voted
127
+ azdo pr reviewers list --pr-number 64 --json # { pullRequestId, reviewers: [{ id, uniqueName, isRequired, vote, voteState, ... }] }
124
128
 
125
129
  # Any pr subcommand can target another repository
126
130
  azdo pr comments --repo other-repo --pr-number 12
package/dist/index.js CHANGED
@@ -3704,7 +3704,12 @@ function mapPullRequest(context, repo, pullRequest) {
3704
3704
  url: pullRequest._links?.web?.href ?? buildPullRequestWebUrl(context, repo, pullRequest.pullRequestId),
3705
3705
  description: pullRequest.description?.trim() || null,
3706
3706
  createdByUniqueName: pullRequest.createdBy?.uniqueName ?? null,
3707
- createdById: pullRequest.createdBy?.id ?? null
3707
+ createdById: pullRequest.createdBy?.id ?? null,
3708
+ isDraft: pullRequest.isDraft ?? false,
3709
+ creationDate: pullRequest.creationDate ?? null,
3710
+ closedDate: pullRequest.closedDate ?? null,
3711
+ reviewers: (pullRequest.reviewers ?? []).map(mapReviewer),
3712
+ labels: (pullRequest.labels ?? []).filter((label) => label.active !== false).map((label) => label.name)
3708
3713
  };
3709
3714
  }
3710
3715
  function mapPullRequestCheckName(status2) {
@@ -3884,6 +3889,15 @@ async function listRepositoryPullRequests(context, repo, cred, opts) {
3884
3889
  const data = await readJsonResponse(response);
3885
3890
  return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
3886
3891
  }
3892
+ async function getPullRequestWorkItemIds(context, repo, cred, prId) {
3893
+ const url = new URL(
3894
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/workitems`
3895
+ );
3896
+ url.searchParams.set("api-version", "7.1");
3897
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3898
+ const data = await readJsonResponse(response);
3899
+ return data.value.map((ref) => Number.parseInt(ref.id, 10)).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
3900
+ }
3887
3901
  async function getPullRequestChecks(context, repo, cred, prId) {
3888
3902
  const response = await fetchWithErrors(
3889
3903
  buildPullRequestStatusesUrl(context, repo, prId).toString(),
@@ -4247,13 +4261,34 @@ function buildPullRequestReviewerUrl(context, repo, prId, reviewerId) {
4247
4261
  url.searchParams.set("api-version", "7.1");
4248
4262
  return url;
4249
4263
  }
4264
+ function reviewerVoteState(vote) {
4265
+ switch (vote) {
4266
+ case 10:
4267
+ return "approved";
4268
+ case 5:
4269
+ return "approved-with-suggestions";
4270
+ case 0:
4271
+ return "no-vote";
4272
+ case -5:
4273
+ return "waiting-for-author";
4274
+ case -10:
4275
+ return "rejected";
4276
+ case 15:
4277
+ return "bypassed";
4278
+ default:
4279
+ return "unknown";
4280
+ }
4281
+ }
4250
4282
  function mapReviewer(data) {
4283
+ const vote = data.vote ?? 0;
4251
4284
  return {
4252
4285
  id: data.id,
4253
4286
  displayName: data.displayName ?? null,
4254
4287
  uniqueName: data.uniqueName ?? null,
4255
4288
  isRequired: data.isRequired ?? false,
4256
- vote: data.vote ?? 0
4289
+ vote,
4290
+ voteState: reviewerVoteState(vote),
4291
+ hasDeclined: data.hasDeclined ?? false
4257
4292
  };
4258
4293
  }
4259
4294
  async function getPullRequestReviewers(context, repo, cred, prId) {
@@ -5811,42 +5846,73 @@ function createPrCommentDeleteCommand() {
5811
5846
  }
5812
5847
  var LIST_STATUS_VALUES = ["active", "completed", "abandoned", "all"];
5813
5848
  var DEFAULT_LIST_TOP = 25;
5849
+ var WORK_ITEM_LOOKUP_CONCURRENCY = 5;
5814
5850
  function formatPullRequestListEntry(pullRequest) {
5815
- return [
5816
- `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
5851
+ const state = pullRequest.isDraft ? `${pullRequest.status}, draft` : pullRequest.status;
5852
+ const lines = [
5853
+ `#${pullRequest.id} [${state}] ${pullRequest.title}`,
5817
5854
  ` ${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
5818
- ` Author: ${pullRequest.createdBy ?? "Unknown"}`,
5819
- ` ${pullRequest.url ?? "\u2014"}`
5820
- ].join("\n");
5855
+ ` Author: ${pullRequest.createdBy ?? "Unknown"}`
5856
+ ];
5857
+ if ("workItemIds" in pullRequest) {
5858
+ const ids = pullRequest.workItemIds.map((id) => `#${id}`).join(", ");
5859
+ lines.push(` Work items: ${ids || "none"}`);
5860
+ }
5861
+ lines.push(` ${pullRequest.url ?? "\u2014"}`);
5862
+ return lines.join("\n");
5863
+ }
5864
+ function parseListOptions(options) {
5865
+ const status2 = options.status ?? "active";
5866
+ if (!LIST_STATUS_VALUES.includes(status2)) {
5867
+ writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
5868
+ return null;
5869
+ }
5870
+ let top = DEFAULT_LIST_TOP;
5871
+ if (options.top !== void 0) {
5872
+ const parsed = parsePositivePrNumber(options.top);
5873
+ if (parsed === null) {
5874
+ writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
5875
+ return null;
5876
+ }
5877
+ top = parsed;
5878
+ }
5879
+ const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
5880
+ return { status: status2, top, branch };
5881
+ }
5882
+ async function attachWorkItemIds(context, repo, cred, pullRequests) {
5883
+ const result = new Array(pullRequests.length);
5884
+ let next = 0;
5885
+ const worker = async () => {
5886
+ while (next < pullRequests.length) {
5887
+ const index = next;
5888
+ next += 1;
5889
+ const pullRequest = pullRequests[index];
5890
+ const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
5891
+ result[index] = { ...pullRequest, workItemIds };
5892
+ }
5893
+ };
5894
+ await Promise.all(Array.from({ length: Math.min(WORK_ITEM_LOOKUP_CONCURRENCY, pullRequests.length) }, worker));
5895
+ return result;
5821
5896
  }
5822
5897
  function createPrListCommand() {
5823
5898
  const command = new Command12("list");
5824
- withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull requests in the repository, optionally filtered by source branch").option("--branch <name>", "only pull requests whose source branch is this one (with or without the refs/heads/ prefix)").option("--status <status>", `pull request status filter (${LIST_STATUS_VALUES.join(" | ")})`, "active").option("--top <N>", `maximum number of pull requests to return (default ${DEFAULT_LIST_TOP})`).option("--json", "output JSON").action(async (options) => {
5899
+ withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull requests in the repository, optionally filtered by source branch").option("--branch <name>", "only pull requests whose source branch is this one (with or without the refs/heads/ prefix)").option("--status <status>", `pull request status filter (${LIST_STATUS_VALUES.join(" | ")})`, "active").option("--top <N>", `maximum number of pull requests to return (default ${DEFAULT_LIST_TOP})`).option("--work-items", "also return the ids of each pull request's linked work items (one extra API call per pull request)").option("--json", "output JSON").action(async (options) => {
5825
5900
  validateOrgProjectPair(options);
5826
- const status2 = options.status ?? "active";
5827
- if (!LIST_STATUS_VALUES.includes(status2)) {
5828
- writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
5901
+ const parsed = parseListOptions(options);
5902
+ if (parsed === null) {
5829
5903
  return;
5830
5904
  }
5831
- let top = DEFAULT_LIST_TOP;
5832
- if (options.top !== void 0) {
5833
- const parsed = parsePositivePrNumber(options.top);
5834
- if (parsed === null) {
5835
- writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
5836
- return;
5837
- }
5838
- top = parsed;
5839
- }
5840
- const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
5905
+ const { status: status2, top, branch } = parsed;
5841
5906
  let context;
5842
5907
  try {
5843
5908
  const resolved = await resolvePrCommandContext(options, { requireBranch: false });
5844
5909
  context = resolved.context;
5845
- const pullRequests = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
5910
+ const listed = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
5846
5911
  sourceBranch: branch ?? void 0,
5847
5912
  status: status2,
5848
5913
  top
5849
5914
  });
5915
+ const pullRequests = options.workItems ? await attachWorkItemIds(resolved.context, resolved.repo, resolved.pat, listed) : listed;
5850
5916
  const result = {
5851
5917
  repository: resolved.repo,
5852
5918
  branch,
@@ -6042,9 +6108,50 @@ async function runReviewerRemove(reviewer, options) {
6042
6108
  handlePrCommandError(err, context, "write");
6043
6109
  }
6044
6110
  }
6111
+ function formatReviewerLine(reviewer) {
6112
+ const name = reviewer.displayName ?? reviewer.uniqueName ?? reviewer.id;
6113
+ const unique = reviewer.uniqueName && reviewer.uniqueName !== name ? ` <${reviewer.uniqueName}>` : "";
6114
+ const kind = reviewer.isRequired ? "required" : "optional";
6115
+ const declined = reviewer.hasDeclined ? ", declined" : "";
6116
+ return `${name}${unique} \u2014 ${reviewer.voteState} (${kind}${declined})`;
6117
+ }
6118
+ async function runReviewerList(options) {
6119
+ let context;
6120
+ try {
6121
+ const target = await resolvePullRequestTarget(options, {
6122
+ onContextResolved: (resolved) => {
6123
+ context = resolved;
6124
+ }
6125
+ });
6126
+ if (target === null) {
6127
+ return;
6128
+ }
6129
+ const reviewers = await getPullRequestReviewers(target.context, target.repo, target.pat, target.pullRequest.id);
6130
+ const result = { pullRequestId: target.pullRequest.id, reviewers };
6131
+ if (options.json) {
6132
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
6133
+ `);
6134
+ return;
6135
+ }
6136
+ if (reviewers.length === 0) {
6137
+ process.stdout.write(`No reviewers on pull request #${target.pullRequest.id}.
6138
+ `);
6139
+ return;
6140
+ }
6141
+ process.stdout.write(`${reviewers.map(formatReviewerLine).join("\n")}
6142
+ `);
6143
+ } catch (err) {
6144
+ handlePrCommandError(err, context, "read");
6145
+ }
6146
+ }
6045
6147
  function createPrReviewersCommand() {
6046
6148
  const command = new Command12("reviewers");
6047
6149
  command.description("Manage pull request reviewers");
6150
+ const list = new Command12("list");
6151
+ withCommonPrOptions(configureUnwrappedHelp(list)).description("List the pull request reviewers with their votes (approved, waiting-for-author, \u2026) and required flag").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
6152
+ await runReviewerList(mergedPrOptions(command2));
6153
+ });
6154
+ command.addCommand(list);
6048
6155
  const add = new Command12("add");
6049
6156
  withCommonPrOptions(configureUnwrappedHelp(add)).description("Add a reviewer to the pull request (optional by default)").argument("<reviewer>", "reviewer email or unique name").option("--required", "mark the reviewer as required instead of optional").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
6050
6157
  await runReviewerAdd(reviewer, mergedPrOptions(command2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.22.0-develop.787",
3
+ "version": "0.22.0-develop.793",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {