azdo-cli 0.21.0 → 0.22.0-ci-harden-workflow.876

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.
package/dist/index.js CHANGED
@@ -39,10 +39,10 @@ import {
39
39
  tokenResponseToCredential,
40
40
  unsetConfigValue,
41
41
  unsetOrgScopedValue
42
- } from "./chunk-W4EVKARR.js";
42
+ } from "./chunk-MHJ6OBX3.js";
43
43
 
44
44
  // src/program.ts
45
- import { Command as Command19 } from "commander";
45
+ import { Command as Command20 } from "commander";
46
46
 
47
47
  // src/version.ts
48
48
  import { readFileSync } from "fs";
@@ -143,6 +143,7 @@ function getActiveTraceWriter() {
143
143
  }
144
144
 
145
145
  // src/services/command-helpers.ts
146
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
146
147
  var EXIT_CREDENTIAL_STORE_UNAVAILABLE = 4;
147
148
  function reportCredentialStoreUnavailable(err) {
148
149
  if (!(err instanceof CredentialStoreUnavailableError)) return false;
@@ -249,8 +250,9 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
249
250
  );
250
251
  writeErrorDetail(msg, "PERMISSION_DENIED");
251
252
  } else if (msg.startsWith("NOT_FOUND")) {
253
+ const subject = id === void 0 ? "Project or resource" : `Work item ${id}`;
252
254
  process.stderr.write(
253
- `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
255
+ `Error: ${subject} not found in ${context?.org}/${context?.project}.
254
256
  `
255
257
  );
256
258
  } else if (msg === "NETWORK_ERROR") {
@@ -278,11 +280,29 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
278
280
  process.exitCode = 1;
279
281
  }
280
282
  }
283
+ var STDIN_PATH = "-";
284
+ function readTextSource(file) {
285
+ if (file === STDIN_PATH) {
286
+ try {
287
+ return readFileSync2(0, "utf-8");
288
+ } catch {
289
+ throw new Error("Cannot read standard input.");
290
+ }
291
+ }
292
+ if (!existsSync(file)) {
293
+ throw new Error(`File not found: ${file}`);
294
+ }
295
+ try {
296
+ return readFileSync2(file, "utf-8");
297
+ } catch {
298
+ throw new Error(`Cannot read file: ${file}`);
299
+ }
300
+ }
281
301
 
282
302
  // src/services/image-download.ts
283
303
  import { Jimp } from "jimp";
284
304
  import { writeFile } from "fs/promises";
285
- import { existsSync } from "fs";
305
+ import { existsSync as existsSync2 } from "fs";
286
306
  import { tmpdir } from "os";
287
307
  import { join } from "path";
288
308
  var ATTACHMENT_GUID_RE = /_apis\/wit\/attachments\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
@@ -365,7 +385,7 @@ function resolveImageDownloadOptions(flags) {
365
385
  maxWidth = parsed;
366
386
  }
367
387
  const outputDir = flags.imagesPath ?? tmpdir();
368
- if (flags.imagesPath !== void 0 && !existsSync(outputDir)) {
388
+ if (flags.imagesPath !== void 0 && !existsSync2(outputDir)) {
369
389
  throw new Error(`Images path "${outputDir}" does not exist.`);
370
390
  }
371
391
  return { enabled, maxWidth, outputDir };
@@ -455,6 +475,9 @@ var DEFAULT_FIELDS = [
455
475
  "System.State",
456
476
  "System.WorkItemType",
457
477
  "System.AssignedTo",
478
+ "System.CreatedBy",
479
+ "System.CreatedDate",
480
+ "System.Tags",
458
481
  "System.Description",
459
482
  "Microsoft.VSTS.Common.AcceptanceCriteria",
460
483
  "Microsoft.VSTS.TCM.ReproSteps",
@@ -667,12 +690,19 @@ function buildWorkItemCommentsUrl(context, id) {
667
690
  url.searchParams.set("api-version", "7.1-preview.4");
668
691
  return url;
669
692
  }
693
+ function buildWorkItemCommentUrl(context, id, commentId) {
694
+ const url = buildWorkItemCommentsUrl(context, id);
695
+ url.pathname = `${url.pathname}/${commentId}`;
696
+ return url;
697
+ }
670
698
  function mapWorkItemComment(comment, fallbackWorkItemId) {
671
699
  return {
672
700
  id: comment.id ?? comment.commentId ?? 0,
673
701
  workItemId: comment.workItemId ?? fallbackWorkItemId,
674
702
  text: typeof comment.text === "string" ? comment.text : "",
675
703
  author: comment.createdBy?.displayName ?? null,
704
+ authorUniqueName: comment.createdBy?.uniqueName ?? null,
705
+ authorId: comment.createdBy?.id ?? null,
676
706
  createdAt: comment.createdDate ?? null,
677
707
  modifiedAt: comment.modifiedDate ?? null,
678
708
  isDeleted: comment.isDeleted === true
@@ -782,6 +812,30 @@ async function getOrgFieldNames(context, cred) {
782
812
  const data = await response.json();
783
813
  return (data.value ?? []).map((f) => f.referenceName);
784
814
  }
815
+ function toIdentity(ref) {
816
+ if (!ref) return null;
817
+ return {
818
+ displayName: ref.displayName ?? null,
819
+ uniqueName: ref.uniqueName ?? null,
820
+ id: ref.id ?? null
821
+ };
822
+ }
823
+ var PULL_REQUEST_ARTIFACT = /^vstfs:\/\/\/Git\/PullRequestId\/(.+)$/i;
824
+ function mapRelations(relations) {
825
+ return (relations ?? []).map((r) => {
826
+ const entry = { rel: r.rel, name: r.attributes?.name ?? null, url: r.url };
827
+ const workItemMatch = /\/workItems\/(\d+)$/i.exec(r.url);
828
+ if (workItemMatch) entry.workItemId = Number(workItemMatch[1]);
829
+ const prMatch = PULL_REQUEST_ARTIFACT.exec(r.url);
830
+ if (prMatch) {
831
+ const [projectId, repositoryId, prId] = prMatch[1].split(/%2F|\//i);
832
+ if (projectId && repositoryId && /^\d+$/.test(prId ?? "")) {
833
+ entry.pullRequest = { id: Number(prId), repositoryId, projectId };
834
+ }
835
+ }
836
+ return entry;
837
+ });
838
+ }
785
839
  function buildCombinedDescription(fields) {
786
840
  const parts = [];
787
841
  if (fields["System.Description"]) {
@@ -837,6 +891,11 @@ async function getWorkItem(context, id, cred, extraFields) {
837
891
  state: data.fields["System.State"],
838
892
  type: data.fields["System.WorkItemType"],
839
893
  assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
894
+ assignedToIdentity: toIdentity(data.fields["System.AssignedTo"]),
895
+ createdBy: toIdentity(data.fields["System.CreatedBy"]),
896
+ createdDate: data.fields["System.CreatedDate"] ?? null,
897
+ tags: parseTags(data.fields["System.Tags"]),
898
+ relations: mapRelations(relationsData.relations),
840
899
  description: buildCombinedDescription(data.fields),
841
900
  areaPath: data.fields["System.AreaPath"],
842
901
  iterationPath: data.fields["System.IterationPath"],
@@ -891,16 +950,11 @@ async function listWorkItemComments(context, id, cred) {
891
950
  comments
892
951
  };
893
952
  }
894
- async function addWorkItemComment(context, id, cred, text, format = "html") {
895
- const url = buildWorkItemCommentsUrl(context, id);
896
- url.searchParams.set("format", format);
953
+ async function sendCommentRequest(url, cred, method, body) {
897
954
  const response = await fetchWithErrors(url.toString(), {
898
- method: "POST",
899
- headers: {
900
- ...authHeaders(cred),
901
- "Content-Type": "application/json"
902
- },
903
- body: JSON.stringify({ text })
955
+ method,
956
+ headers: body === void 0 ? authHeaders(cred) : { ...authHeaders(cred), "Content-Type": "application/json" },
957
+ body: body === void 0 ? void 0 : JSON.stringify(body)
904
958
  });
905
959
  if (response.status === 400) {
906
960
  const serverMessage = await readResponseMessage(response) ?? "Unknown error";
@@ -909,16 +963,54 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
909
963
  if (!response.ok) {
910
964
  throw httpError(response);
911
965
  }
966
+ return response;
967
+ }
968
+ async function writeCommentText(url, cred, method, text, format, fallback) {
969
+ url.searchParams.set("format", format);
970
+ const response = await sendCommentRequest(url, cred, method, { text });
912
971
  const data = await response.json();
913
972
  return {
914
- workItemId: data.workItemId ?? id,
915
- commentId: data.commentId ?? data.id ?? 0,
973
+ workItemId: data.workItemId ?? fallback.workItemId,
974
+ commentId: data.commentId ?? data.id ?? fallback.commentId,
916
975
  text: typeof data.text === "string" ? data.text : text,
917
976
  author: data.createdBy?.displayName ?? null,
977
+ authorUniqueName: data.createdBy?.uniqueName ?? null,
978
+ authorId: data.createdBy?.id ?? null,
918
979
  createdAt: data.createdDate ?? null,
980
+ modifiedAt: data.modifiedDate ?? null,
919
981
  url: data.url ?? null
920
982
  };
921
983
  }
984
+ async function addWorkItemComment(context, id, cred, text, format = "html") {
985
+ const written = await writeCommentText(
986
+ buildWorkItemCommentsUrl(context, id),
987
+ cred,
988
+ "POST",
989
+ text,
990
+ format,
991
+ { workItemId: id, commentId: 0 }
992
+ );
993
+ return {
994
+ workItemId: written.workItemId,
995
+ commentId: written.commentId,
996
+ text: written.text,
997
+ author: written.author,
998
+ authorUniqueName: written.authorUniqueName,
999
+ authorId: written.authorId,
1000
+ createdAt: written.createdAt,
1001
+ url: written.url
1002
+ };
1003
+ }
1004
+ async function updateWorkItemComment(context, id, commentId, cred, text, format = "html") {
1005
+ return writeCommentText(buildWorkItemCommentUrl(context, id, commentId), cred, "PATCH", text, format, {
1006
+ workItemId: id,
1007
+ commentId
1008
+ });
1009
+ }
1010
+ async function deleteWorkItemComment(context, id, commentId, cred) {
1011
+ await sendCommentRequest(buildWorkItemCommentUrl(context, id, commentId), cred, "DELETE");
1012
+ return { workItemId: id, commentId, deleted: true };
1013
+ }
922
1014
  async function updateWorkItem(context, id, cred, fieldName, operations) {
923
1015
  const result = await applyWorkItemPatch(context, id, cred, operations);
924
1016
  const title = result.fields["System.Title"];
@@ -988,10 +1080,111 @@ async function createAttachment(context, fileName, content, cred) {
988
1080
  }
989
1081
  return await response.json();
990
1082
  }
1083
+ var LIST_SYSTEM_FIELDS = [
1084
+ "System.Title",
1085
+ "System.State",
1086
+ "System.WorkItemType",
1087
+ "System.AssignedTo",
1088
+ "System.Description",
1089
+ "System.Tags",
1090
+ "System.TeamProject"
1091
+ ];
1092
+ var LIST_PROCESS_FIELDS = [
1093
+ "Microsoft.VSTS.Common.AcceptanceCriteria",
1094
+ "Microsoft.VSTS.TCM.ReproSteps"
1095
+ ];
1096
+ var WORKITEMS_BATCH_LIMIT = 200;
1097
+ function wiqlLiteral(value) {
1098
+ return `'${value.replaceAll("'", "''")}'`;
1099
+ }
1100
+ function buildListWiql(filter) {
1101
+ const clauses = ["[System.TeamProject] = @project"];
1102
+ if (filter.state) clauses.push(`[System.State] = ${wiqlLiteral(filter.state)}`);
1103
+ if (filter.tag) clauses.push(`[System.Tags] CONTAINS ${wiqlLiteral(filter.tag)}`);
1104
+ if (filter.assignedTo) {
1105
+ clauses.push(
1106
+ filter.assignedTo.toLowerCase() === "@me" ? "[System.AssignedTo] = @Me" : `[System.AssignedTo] = ${wiqlLiteral(filter.assignedTo)}`
1107
+ );
1108
+ }
1109
+ if (filter.titleContains) clauses.push(`[System.Title] CONTAINS ${wiqlLiteral(filter.titleContains)}`);
1110
+ return `SELECT [System.Id] FROM WorkItems WHERE ${clauses.join(" AND ")} ORDER BY [System.ChangedDate] DESC`;
1111
+ }
1112
+ function parseTags(raw) {
1113
+ if (typeof raw !== "string") return [];
1114
+ return raw.split(";").map((t) => t.trim()).filter((t) => t.length > 0);
1115
+ }
1116
+ async function runWiql(context, cred, wiql, top) {
1117
+ const url = new URL(
1118
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/wiql`
1119
+ );
1120
+ url.searchParams.set("api-version", "7.1");
1121
+ url.searchParams.set("$top", String(top));
1122
+ const response = await fetchWithErrors(url.toString(), {
1123
+ method: "POST",
1124
+ headers: { ...authHeaders(cred), "Content-Type": "application/json" },
1125
+ body: JSON.stringify({ query: wiql })
1126
+ });
1127
+ if (response.status === 400) {
1128
+ const serverMessage = await readResponseMessage(response);
1129
+ if (serverMessage) throw new Error(`BAD_REQUEST: ${serverMessage}`);
1130
+ }
1131
+ if (!response.ok) throw httpError(response);
1132
+ const data = await response.json();
1133
+ return (data.workItems ?? []).map((w) => w.id);
1134
+ }
1135
+ async function postWorkItemsBatch(context, cred, ids, fields) {
1136
+ const url = new URL(
1137
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitemsbatch`
1138
+ );
1139
+ url.searchParams.set("api-version", "7.1");
1140
+ return fetchWithErrors(url.toString(), {
1141
+ method: "POST",
1142
+ headers: { ...authHeaders(cred), "Content-Type": "application/json" },
1143
+ body: JSON.stringify({ ids, fields })
1144
+ });
1145
+ }
1146
+ async function readWorkItemsBatch(context, cred, ids) {
1147
+ let response = await postWorkItemsBatch(context, cred, ids, [...LIST_SYSTEM_FIELDS, ...LIST_PROCESS_FIELDS]);
1148
+ if (response.status === 400) {
1149
+ const serverMessage = await readResponseMessage(response);
1150
+ if (!serverMessage?.includes("TF51535")) {
1151
+ throw serverMessage ? new Error(`BAD_REQUEST: ${serverMessage}`) : httpError(response);
1152
+ }
1153
+ response = await postWorkItemsBatch(context, cred, ids, LIST_SYSTEM_FIELDS);
1154
+ }
1155
+ if (!response.ok) throw httpError(response);
1156
+ return (await response.json()).value ?? [];
1157
+ }
1158
+ async function queryWorkItems(context, cred, filter) {
1159
+ const ids = await runWiql(context, cred, buildListWiql(filter), filter.top);
1160
+ const byId = /* @__PURE__ */ new Map();
1161
+ const chunks = [];
1162
+ for (let i = 0; i < ids.length; i += WORKITEMS_BATCH_LIMIT) {
1163
+ chunks.push(ids.slice(i, i + WORKITEMS_BATCH_LIMIT));
1164
+ }
1165
+ const batches = await Promise.all(chunks.map((chunk) => readWorkItemsBatch(context, cred, chunk)));
1166
+ for (const batch of batches) {
1167
+ for (const item of batch) {
1168
+ const teamProject = item.fields["System.TeamProject"];
1169
+ const project = typeof teamProject === "string" ? teamProject : context.project;
1170
+ byId.set(item.id, {
1171
+ id: item.id,
1172
+ title: item.fields["System.Title"],
1173
+ description: buildCombinedDescription(item.fields),
1174
+ url: `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(project)}/_workitems/edit/${item.id}`,
1175
+ state: item.fields["System.State"],
1176
+ type: item.fields["System.WorkItemType"],
1177
+ tags: parseTags(item.fields["System.Tags"]),
1178
+ assignedTo: item.fields["System.AssignedTo"]?.displayName ?? null
1179
+ });
1180
+ }
1181
+ }
1182
+ return ids.flatMap((id) => byId.get(id) ?? []);
1183
+ }
991
1184
 
992
1185
  // src/services/auth.ts
993
1186
  import { createInterface } from "readline";
994
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1187
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
995
1188
  import { dirname as dirname2, join as join2 } from "path";
996
1189
 
997
1190
  // src/services/oauth-device-code.ts
@@ -1161,8 +1354,8 @@ async function promptForPat() {
1161
1354
  });
1162
1355
  }
1163
1356
  function patFromEnvFile(envFile) {
1164
- if (!existsSync2(envFile)) return null;
1165
- const contents = readFileSync2(envFile, "utf8");
1357
+ if (!existsSync3(envFile)) return null;
1358
+ const contents = readFileSync3(envFile, "utf8");
1166
1359
  for (const line of contents.split("\n")) {
1167
1360
  const match = line.match(/^AZDO_PAT\s*=([^\n\r]+)$/);
1168
1361
  if (match === null) continue;
@@ -1846,6 +2039,20 @@ function convertRichText(html, markdown) {
1846
2039
  if (!html) return "";
1847
2040
  return markdown ? toMarkdown(html) : stripHtml(html);
1848
2041
  }
2042
+ function toJsonDocument(workItem) {
2043
+ return {
2044
+ id: workItem.id,
2045
+ title: workItem.title,
2046
+ description: workItem.description ? toMarkdown(workItem.description) : "",
2047
+ state: workItem.state,
2048
+ tags: workItem.tags ?? [],
2049
+ assignedTo: workItem.assignedToIdentity ?? null,
2050
+ createdBy: workItem.createdBy ?? null,
2051
+ createdDate: workItem.createdDate ?? null,
2052
+ url: workItem.url,
2053
+ relations: workItem.relations ?? []
2054
+ };
2055
+ }
1849
2056
  function formatMarkdownField(fieldLabel, value) {
1850
2057
  if (value.includes("\n")) {
1851
2058
  return `${fieldLabel}:
@@ -1923,13 +2130,17 @@ function formatWorkItem(workItem, short, markdown = false) {
1923
2130
  }
1924
2131
  function createGetItemCommand() {
1925
2132
  const command = new Command("get-item");
1926
- command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown");
2133
+ command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown").option("--json", "output { id, title, description (markdown), state, tags, assignedTo, createdBy, createdDate, url, relations } (assignedTo/createdBy: { displayName, uniqueName, id }; relations include pull requests)");
1927
2134
  addImageDownloadOptions(command);
1928
2135
  command.action(
1929
2136
  async (idStr, options) => {
1930
2137
  const id = parseWorkItemId(idStr);
1931
2138
  validateOrgProjectPair(options);
1932
2139
  const imageOptions = resolveImageDownloadOptionsOrExit(options);
2140
+ if (options.json && (imageOptions.enabled || options.downloadImages || options.resizeImages !== void 0 || options.imagesPath !== void 0)) {
2141
+ process.stderr.write("Error: --json cannot be combined with image download options.\n");
2142
+ process.exit(1);
2143
+ }
1933
2144
  let context;
1934
2145
  try {
1935
2146
  context = resolveContext(options);
@@ -1937,6 +2148,10 @@ function createGetItemCommand() {
1937
2148
  const scopedCfg = resolveScopedConfig(context.org);
1938
2149
  const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
1939
2150
  const workItem = await getWorkItem(context, id, credential, fieldsList);
2151
+ if (options.json) {
2152
+ process.stdout.write(JSON.stringify(toJsonDocument(workItem)) + "\n");
2153
+ return;
2154
+ }
1940
2155
  const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
1941
2156
  const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
1942
2157
  process.stdout.write(output + "\n");
@@ -1957,10 +2172,69 @@ function createGetItemCommand() {
1957
2172
  return command;
1958
2173
  }
1959
2174
 
1960
- // src/commands/clear-pat.ts
2175
+ // src/commands/list-items.ts
1961
2176
  import { Command as Command2 } from "commander";
2177
+ var DEFAULT_TOP = 50;
2178
+ var MAX_TOP = 1e3;
2179
+ function parseTop(raw) {
2180
+ if (raw === void 0) return DEFAULT_TOP;
2181
+ const top = Number(raw);
2182
+ if (!/^\d+$/.test(raw) || top < 1 || top > MAX_TOP) {
2183
+ process.stderr.write(`Error: --top must be an integer between 1 and ${MAX_TOP}. Got: "${raw}"
2184
+ `);
2185
+ process.exit(1);
2186
+ }
2187
+ return top;
2188
+ }
2189
+ function toJsonRow(item) {
2190
+ return {
2191
+ id: item.id,
2192
+ title: item.title,
2193
+ description: item.description ? toMarkdown(item.description) : "",
2194
+ url: item.url,
2195
+ state: item.state,
2196
+ tags: item.tags,
2197
+ assignedTo: item.assignedTo
2198
+ };
2199
+ }
2200
+ function formatItemLine(item) {
2201
+ const tags = item.tags.length > 0 ? ` [${item.tags.join(", ")}]` : "";
2202
+ return `${item.id} ${item.state} ${item.assignedTo ?? "Unassigned"} ${item.title}${tags}`;
2203
+ }
2204
+ function createListItemsCommand() {
2205
+ const command = new Command2("list-items");
2206
+ command.description("List work items filtered by state, tag, assignee and title (one WIQL query, then batched reads of 200 items)").option("--state <state>", 'only items in this state (e.g. "Active")').option("--tag <tag>", "only items carrying this tag").option("--assigned-to <user>", "only items assigned to this user (display name, email, or @me)").option("--title-contains <text>", "only items whose title contains this text").option("--top <n>", `maximum number of items, newest change first (default ${DEFAULT_TOP}, max ${MAX_TOP})`).option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output an array of { id, title, description (markdown), url, state, tags, assignedTo }").action(async (options) => {
2207
+ validateOrgProjectPair(options);
2208
+ const top = parseTop(options.top);
2209
+ let context;
2210
+ try {
2211
+ context = resolveContext(options);
2212
+ const credential = await requireAuthCredential(context.org);
2213
+ const items = await queryWorkItems(context, credential, {
2214
+ state: options.state,
2215
+ tag: options.tag,
2216
+ assignedTo: options.assignedTo,
2217
+ titleContains: options.titleContains,
2218
+ top
2219
+ });
2220
+ if (options.json) {
2221
+ process.stdout.write(JSON.stringify(items.map(toJsonRow)) + "\n");
2222
+ } else if (items.length === 0) {
2223
+ process.stdout.write("No work items found.\n");
2224
+ } else {
2225
+ process.stdout.write(items.map(formatItemLine).join("\n") + "\n");
2226
+ }
2227
+ } catch (err) {
2228
+ handleCommandError(err, void 0, context, "read");
2229
+ }
2230
+ });
2231
+ return command;
2232
+ }
2233
+
2234
+ // src/commands/clear-pat.ts
2235
+ import { Command as Command3 } from "commander";
1962
2236
  function createClearPatCommand() {
1963
- const command = new Command2("clear-pat");
2237
+ const command = new Command3("clear-pat");
1964
2238
  command.description("Remove a stored Azure DevOps PAT (deprecated: use `azdo auth logout`)").option("--org <name>", "Azure DevOps organization (overrides auto-detect / config)").action(async (options) => {
1965
2239
  process.stderr.write("`azdo clear-pat` is deprecated; use `azdo auth logout [--org <name>]` instead.\n");
1966
2240
  const resolved = resolveOrg({ org: options.org });
@@ -1983,7 +2257,7 @@ function createClearPatCommand() {
1983
2257
  }
1984
2258
 
1985
2259
  // src/commands/auth.ts
1986
- import { Command as Command3 } from "commander";
2260
+ import { Command as Command4 } from "commander";
1987
2261
 
1988
2262
  // src/services/auth-diagnostics.ts
1989
2263
  async function runConnectivityTest(org, cred) {
@@ -2481,7 +2755,7 @@ async function handleToken(orgFromGlobal) {
2481
2755
  `);
2482
2756
  }
2483
2757
  function createAuthCommand() {
2484
- const command = new Command3("auth");
2758
+ const command = new Command4("auth");
2485
2759
  command.description(
2486
2760
  "Manage Azure DevOps authentication. Use `azdo auth login` for OAuth (default); the bare `azdo auth` form preserves the legacy PAT-prompt path for back-compat."
2487
2761
  );
@@ -2619,7 +2893,7 @@ not be resolved, 4 OS credential store unavailable. stdout stays empty on all.
2619
2893
  }
2620
2894
 
2621
2895
  // src/commands/config.ts
2622
- import { Command as Command4 } from "commander";
2896
+ import { Command as Command5 } from "commander";
2623
2897
  import { createInterface as createInterface2 } from "readline";
2624
2898
 
2625
2899
  // src/services/dpapi-copy.ts
@@ -2777,9 +3051,43 @@ async function promptForSetting(cfg, setting, ask) {
2777
3051
 
2778
3052
  `);
2779
3053
  }
3054
+ var SETTING_TYPE_LABEL = {
3055
+ string: "string",
3056
+ "string[]": "comma-separated list",
3057
+ boolean: "true | false"
3058
+ };
3059
+ function describeSettingValues(setting) {
3060
+ if (!setting.values) return `type: ${SETTING_TYPE_LABEL[setting.type]}`;
3061
+ const list = setting.values.map((v) => v.note ? `${v.value} (${v.note})` : v.value).join(", ");
3062
+ return `values: ${list}`;
3063
+ }
3064
+ function renderSettingsHelp(settings = SETTINGS) {
3065
+ const keyWidth = Math.max(...settings.map((s) => s.key.length)) + 3;
3066
+ const indent = " ".repeat(2 + keyWidth);
3067
+ const lines = ["Settings (azdo config set <key> <value>):", ""];
3068
+ for (const setting of settings) {
3069
+ const facts = [
3070
+ describeSettingValues(setting),
3071
+ `scope: ${setting.scoped ? "global, or per organization with --org <org>" : "global only"}`,
3072
+ ...setting.required ? ["required"] : [],
3073
+ ...setting.env ? [`env: ${setting.env} overrides the stored value`] : [],
3074
+ `example: azdo config set ${setting.key} ${setting.example}`
3075
+ ];
3076
+ lines.push(
3077
+ ` ${setting.key.padEnd(keyWidth)}${setting.description}`,
3078
+ ...facts.map((fact) => `${indent}${fact}`),
3079
+ ""
3080
+ );
3081
+ }
3082
+ return lines.join("\n").trimEnd();
3083
+ }
3084
+ var KEY_ARGUMENT_HELP = `setting key (${SETTINGS.map((s) => s.key).join(", ")})`;
3085
+ var KEY_HELP_POINTER = "\nRun `azdo config --help` for each setting's meaning, accepted values and scope.\n";
2780
3086
  function createConfigCommand() {
2781
- const config = new Command4("config");
3087
+ const config = new Command5("config");
2782
3088
  config.description("Manage CLI settings");
3089
+ config.addHelpText("after", `
3090
+ ${renderSettingsHelp()}`);
2783
3091
  config.addHelpText(
2784
3092
  "after",
2785
3093
  [
@@ -2796,8 +3104,8 @@ function createConfigCommand() {
2796
3104
  "Run `azdo auth diagnose` to see which credential is in use."
2797
3105
  ].join("\n")
2798
3106
  );
2799
- const set = new Command4("set");
2800
- set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").option("--copy-credentials", "with credentialStore dpapi: copy Credential Manager credentials without asking").option("--no-copy-credentials", "with credentialStore dpapi: do not offer to copy Credential Manager credentials").action((key, value, options) => {
3107
+ const set = new Command5("set");
3108
+ set.description("Set a configuration value").argument("<key>", KEY_ARGUMENT_HELP).argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").option("--copy-credentials", "with credentialStore dpapi: copy Credential Manager credentials without asking").option("--no-copy-credentials", "with credentialStore dpapi: do not offer to copy Credential Manager credentials").addHelpText("after", KEY_HELP_POINTER).action((key, value, options) => {
2801
3109
  try {
2802
3110
  if (options.org) {
2803
3111
  setOrgScopedValue(options.org, key, value);
@@ -2830,8 +3138,8 @@ function createConfigCommand() {
2830
3138
  }
2831
3139
  writeJson();
2832
3140
  });
2833
- const get = new Command4("get");
2834
- get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
3141
+ const get = new Command5("get");
3142
+ get.description("Get a configuration value").argument("<key>", KEY_ARGUMENT_HELP).option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").addHelpText("after", KEY_HELP_POINTER).action((key, options) => {
2835
3143
  try {
2836
3144
  const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
2837
3145
  if (options.json) {
@@ -2853,7 +3161,7 @@ function createConfigCommand() {
2853
3161
  process.exit(1);
2854
3162
  }
2855
3163
  });
2856
- const list = new Command4("list");
3164
+ const list = new Command5("list");
2857
3165
  list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
2858
3166
  const cfg = loadConfig();
2859
3167
  if (options.json) {
@@ -2863,8 +3171,8 @@ function createConfigCommand() {
2863
3171
  }
2864
3172
  writeConfigList(cfg);
2865
3173
  });
2866
- const unset = new Command4("unset");
2867
- unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
3174
+ const unset = new Command5("unset");
3175
+ unset.description("Remove a configuration value").argument("<key>", KEY_ARGUMENT_HELP).option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").addHelpText("after", KEY_HELP_POINTER).action((key, options) => {
2868
3176
  try {
2869
3177
  if (options.org) {
2870
3178
  unsetOrgScopedValue(options.org, key);
@@ -2885,7 +3193,7 @@ function createConfigCommand() {
2885
3193
  process.exit(1);
2886
3194
  }
2887
3195
  });
2888
- const wizard = new Command4("wizard");
3196
+ const wizard = new Command5("wizard");
2889
3197
  wizard.description("Interactive wizard to configure all settings").action(async () => {
2890
3198
  if (!process.stdin.isTTY) {
2891
3199
  process.stderr.write(
@@ -2908,7 +3216,7 @@ function createConfigCommand() {
2908
3216
  rl.close();
2909
3217
  process.stderr.write("Configuration complete!\n");
2910
3218
  });
2911
- const orgCopy = new Command4("org-copy");
3219
+ const orgCopy = new Command5("org-copy");
2912
3220
  orgCopy.description('Copy settings from one scope to another (use "default" as source to copy top-level settings)').argument("<from>", 'source scope name or "default"').argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
2913
3221
  try {
2914
3222
  copyOrgScope(from, to, options.force ?? false);
@@ -2921,7 +3229,7 @@ function createConfigCommand() {
2921
3229
  process.exit(1);
2922
3230
  }
2923
3231
  });
2924
- const orgMove = new Command4("org-move");
3232
+ const orgMove = new Command5("org-move");
2925
3233
  orgMove.description("Move settings from one org scope to another").argument("<from>", "source org name").argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
2926
3234
  try {
2927
3235
  moveOrgScope(from, to, options.force ?? false);
@@ -2934,7 +3242,7 @@ function createConfigCommand() {
2934
3242
  process.exit(1);
2935
3243
  }
2936
3244
  });
2937
- const orgDelete = new Command4("org-delete");
3245
+ const orgDelete = new Command5("org-delete");
2938
3246
  orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
2939
3247
  try {
2940
3248
  deleteOrgScope(name);
@@ -2959,9 +3267,9 @@ function createConfigCommand() {
2959
3267
  }
2960
3268
 
2961
3269
  // src/commands/set-state.ts
2962
- import { Command as Command5 } from "commander";
3270
+ import { Command as Command6 } from "commander";
2963
3271
  function createSetStateCommand() {
2964
- const command = new Command5("set-state");
3272
+ const command = new Command6("set-state");
2965
3273
  command.description("Change the state of a work item").argument("<id>", "work item ID").argument("<state>", 'target state (e.g., "Active", "Closed")').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
2966
3274
  async (idStr, state, options) => {
2967
3275
  const id = parseWorkItemId(idStr);
@@ -2997,9 +3305,9 @@ function createSetStateCommand() {
2997
3305
  }
2998
3306
 
2999
3307
  // src/commands/assign.ts
3000
- import { Command as Command6 } from "commander";
3308
+ import { Command as Command7 } from "commander";
3001
3309
  function createAssignCommand() {
3002
- const command = new Command6("assign");
3310
+ const command = new Command7("assign");
3003
3311
  command.description("Assign a work item to a user, or unassign it").argument("<id>", "work item ID").argument("[name]", "user display name or email").option("--unassign", "clear the Assigned To field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
3004
3312
  async (idStr, name, options) => {
3005
3313
  const id = parseWorkItemId(idStr);
@@ -3049,9 +3357,9 @@ function createAssignCommand() {
3049
3357
  }
3050
3358
 
3051
3359
  // src/commands/set-field.ts
3052
- import { Command as Command7 } from "commander";
3360
+ import { Command as Command8 } from "commander";
3053
3361
  function createSetFieldCommand() {
3054
- const command = new Command7("set-field");
3362
+ const command = new Command8("set-field");
3055
3363
  command.description("Set any work item field by its reference name").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Title)").argument("<value>", "new value for the field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
3056
3364
  async (idStr, field, value, options) => {
3057
3365
  const id = parseWorkItemId(idStr);
@@ -3087,9 +3395,9 @@ function createSetFieldCommand() {
3087
3395
  }
3088
3396
 
3089
3397
  // src/commands/get-md-field.ts
3090
- import { Command as Command8 } from "commander";
3398
+ import { Command as Command9 } from "commander";
3091
3399
  function createGetMdFieldCommand() {
3092
- const command = new Command8("get-md-field");
3400
+ const command = new Command9("get-md-field");
3093
3401
  command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project");
3094
3402
  addImageDownloadOptions(command);
3095
3403
  command.action(
@@ -3123,8 +3431,8 @@ function createGetMdFieldCommand() {
3123
3431
  }
3124
3432
 
3125
3433
  // src/commands/set-md-field.ts
3126
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
3127
- import { Command as Command9 } from "commander";
3434
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
3435
+ import { Command as Command10 } from "commander";
3128
3436
  function fail(message) {
3129
3437
  process.stderr.write(`Error: ${message}
3130
3438
  `);
@@ -3143,11 +3451,11 @@ function resolveContent(inlineContent, options) {
3143
3451
  return null;
3144
3452
  }
3145
3453
  function readFileContent(filePath) {
3146
- if (!existsSync3(filePath)) {
3454
+ if (!existsSync4(filePath)) {
3147
3455
  fail(`File not found: ${filePath}`);
3148
3456
  }
3149
3457
  try {
3150
- return readFileSync3(filePath, "utf-8");
3458
+ return readFileSync4(filePath, "utf-8");
3151
3459
  } catch {
3152
3460
  fail(`Cannot read file: ${filePath}`);
3153
3461
  }
@@ -3186,7 +3494,7 @@ function formatOutput(result, options, field) {
3186
3494
  }
3187
3495
  }
3188
3496
  function createSetMdFieldCommand() {
3189
- const command = new Command9("set-md-field");
3497
+ const command = new Command10("set-md-field");
3190
3498
  command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
3191
3499
  async (idStr, field, inlineContent, options) => {
3192
3500
  const id = parseWorkItemId(idStr);
@@ -3212,8 +3520,8 @@ function createSetMdFieldCommand() {
3212
3520
  }
3213
3521
 
3214
3522
  // src/commands/upsert.ts
3215
- import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
3216
- import { Command as Command10 } from "commander";
3523
+ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync } from "fs";
3524
+ import { Command as Command11 } from "commander";
3217
3525
 
3218
3526
  // src/services/task-document.ts
3219
3527
  var FIELD_ALIASES = /* @__PURE__ */ new Map([
@@ -3376,12 +3684,12 @@ function loadSourceContent(options) {
3376
3684
  return { content: options.content };
3377
3685
  }
3378
3686
  const filePath = options.file;
3379
- if (!existsSync4(filePath)) {
3687
+ if (!existsSync5(filePath)) {
3380
3688
  fail2(`File not found: ${filePath}`);
3381
3689
  }
3382
3690
  try {
3383
3691
  return {
3384
- content: readFileSync4(filePath, "utf-8"),
3692
+ content: readFileSync5(filePath, "utf-8"),
3385
3693
  sourceFile: filePath
3386
3694
  };
3387
3695
  } catch {
@@ -3498,7 +3806,7 @@ function handleUpsertError(err, id, context) {
3498
3806
  process.exit(1);
3499
3807
  }
3500
3808
  function createUpsertCommand() {
3501
- const command = new Command10("upsert");
3809
+ const command = new Command11("upsert");
3502
3810
  command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
3503
3811
  validateOrgProjectPair(options);
3504
3812
  const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
@@ -3539,7 +3847,7 @@ function createUpsertCommand() {
3539
3847
  }
3540
3848
 
3541
3849
  // src/commands/list-fields.ts
3542
- import { Command as Command11 } from "commander";
3850
+ import { Command as Command12 } from "commander";
3543
3851
  function stringifyValue(value) {
3544
3852
  if (value === null || value === void 0) return "";
3545
3853
  if (typeof value === "string") return value;
@@ -3573,7 +3881,7 @@ function formatFieldList(fields) {
3573
3881
  }).join("\n");
3574
3882
  }
3575
3883
  function createListFieldsCommand() {
3576
- const command = new Command11("list-fields");
3884
+ const command = new Command12("list-fields");
3577
3885
  command.description("List all fields of an Azure DevOps work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
3578
3886
  async (idStr, options) => {
3579
3887
  const id = parseWorkItemId(idStr);
@@ -3600,8 +3908,7 @@ function createListFieldsCommand() {
3600
3908
  }
3601
3909
 
3602
3910
  // src/commands/pr.ts
3603
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3604
- import { Command as Command12 } from "commander";
3911
+ import { Command as Command13 } from "commander";
3605
3912
 
3606
3913
  // src/services/pr-client.ts
3607
3914
  function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
@@ -3670,7 +3977,12 @@ function mapPullRequest(context, repo, pullRequest) {
3670
3977
  url: pullRequest._links?.web?.href ?? buildPullRequestWebUrl(context, repo, pullRequest.pullRequestId),
3671
3978
  description: pullRequest.description?.trim() || null,
3672
3979
  createdByUniqueName: pullRequest.createdBy?.uniqueName ?? null,
3673
- createdById: pullRequest.createdBy?.id ?? null
3980
+ createdById: pullRequest.createdBy?.id ?? null,
3981
+ isDraft: pullRequest.isDraft ?? false,
3982
+ creationDate: pullRequest.creationDate ?? null,
3983
+ closedDate: pullRequest.closedDate ?? null,
3984
+ reviewers: (pullRequest.reviewers ?? []).map(mapReviewer),
3985
+ labels: (pullRequest.labels ?? []).filter((label) => label.active !== false).map((label) => label.name)
3674
3986
  };
3675
3987
  }
3676
3988
  function mapPullRequestCheckName(status2) {
@@ -3769,6 +4081,8 @@ function mapComment(comment) {
3769
4081
  return {
3770
4082
  id: comment.id,
3771
4083
  author: comment.author?.displayName ?? null,
4084
+ authorUniqueName: comment.author?.uniqueName ?? null,
4085
+ authorId: comment.author?.id ?? null,
3772
4086
  content,
3773
4087
  publishedAt: comment.publishedDate ?? null,
3774
4088
  commentType: comment.commentType ?? null
@@ -3850,6 +4164,15 @@ async function listRepositoryPullRequests(context, repo, cred, opts) {
3850
4164
  const data = await readJsonResponse(response);
3851
4165
  return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
3852
4166
  }
4167
+ async function getPullRequestWorkItemIds(context, repo, cred, prId) {
4168
+ const url = new URL(
4169
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/workitems`
4170
+ );
4171
+ url.searchParams.set("api-version", "7.1");
4172
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
4173
+ const data = await readJsonResponse(response);
4174
+ return data.value.map((ref) => Number.parseInt(ref.id, 10)).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
4175
+ }
3853
4176
  async function getPullRequestChecks(context, repo, cred, prId) {
3854
4177
  const response = await fetchWithErrors(
3855
4178
  buildPullRequestStatusesUrl(context, repo, prId).toString(),
@@ -3921,43 +4244,26 @@ function formatContributions(composed) {
3921
4244
  function describeDescriptionBudget(composed) {
3922
4245
  return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
3923
4246
  }
3924
- async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
3925
- const existing = await listPullRequests(context, repo, cred, sourceBranch, {
3926
- status: "active",
3927
- targetBranch: "develop"
3928
- });
3929
- if (existing.length === 1) {
3930
- return {
3931
- branch: sourceBranch,
3932
- targetBranch: "develop",
3933
- created: false,
3934
- pullRequest: existing[0]
3935
- };
3936
- }
3937
- if (existing.length > 1) {
3938
- throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
3939
- }
3940
- const repository = await getRepository(context, repo, cred);
3941
- const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
3942
- const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
3943
- const composed = composeDescription(description, template);
3944
- if (composed === null) {
3945
- throw new Error("DESCRIPTION_REQUIRED");
3946
- }
3947
- if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
3948
- throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
3949
- }
4247
+ function buildOpenPayload(sourceBranch, targetBranch, title, description, options) {
3950
4248
  const payload = {
3951
4249
  sourceRefName: `refs/heads/${sourceBranch}`,
3952
- targetRefName: "refs/heads/develop",
4250
+ targetRefName: `refs/heads/${targetBranch}`,
3953
4251
  title,
3954
- description: composed.text
4252
+ description
3955
4253
  };
4254
+ if (options.isDraft) {
4255
+ payload.isDraft = true;
4256
+ }
4257
+ if (options.labels && options.labels.length > 0) {
4258
+ payload.labels = options.labels.map((name) => ({ name }));
4259
+ }
4260
+ return payload;
4261
+ }
4262
+ async function createPullRequest(context, repo, cred, payload, composed) {
3956
4263
  const url = new URL(
3957
4264
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
3958
4265
  );
3959
4266
  url.searchParams.set("api-version", "7.1");
3960
- let data;
3961
4267
  try {
3962
4268
  const response = await fetchWithErrors(url.toString(), {
3963
4269
  method: "POST",
@@ -3967,19 +4273,69 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
3967
4273
  },
3968
4274
  body: JSON.stringify(payload)
3969
4275
  });
3970
- data = await readJsonResponse(response);
4276
+ return await readJsonResponse(response);
3971
4277
  } catch (err) {
3972
4278
  if (err instanceof Error && err.message.startsWith("HTTP_400")) {
3973
4279
  throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
3974
4280
  }
3975
4281
  throw err;
3976
4282
  }
3977
- return {
4283
+ }
4284
+ function linkWorkItems(context, repo, cred, prId, workItemIds) {
4285
+ return Promise.all(
4286
+ workItemIds.map(
4287
+ (id) => linkWorkItemToPullRequest(context, repo, cred, prId, id).then(
4288
+ () => ({ id, linked: true }),
4289
+ (err) => ({ id, linked: false, error: err instanceof Error ? err.message : String(err) })
4290
+ )
4291
+ )
4292
+ );
4293
+ }
4294
+ async function openPullRequest(context, repo, cred, sourceBranch, title, description, options = {}) {
4295
+ const targetBranch = options.targetBranch ?? "develop";
4296
+ const existing = await listPullRequests(context, repo, cred, sourceBranch, {
4297
+ status: "active",
4298
+ targetBranch
4299
+ });
4300
+ if (existing.length === 1) {
4301
+ return {
4302
+ branch: sourceBranch,
4303
+ targetBranch,
4304
+ created: false,
4305
+ pullRequest: existing[0]
4306
+ };
4307
+ }
4308
+ if (existing.length > 1) {
4309
+ throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest2) => pullRequest2.id).join(",")}`);
4310
+ }
4311
+ const repository = await getRepository(context, repo, cred);
4312
+ const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
4313
+ const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch);
4314
+ const composed = composeDescription(description, template);
4315
+ if (composed === null) {
4316
+ throw new Error("DESCRIPTION_REQUIRED");
4317
+ }
4318
+ if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
4319
+ throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
4320
+ }
4321
+ const data = await createPullRequest(
4322
+ context,
4323
+ repo,
4324
+ cred,
4325
+ buildOpenPayload(sourceBranch, targetBranch, title, composed.text, options),
4326
+ composed
4327
+ );
4328
+ const pullRequest = mapPullRequest(context, repo, data);
4329
+ const result = {
3978
4330
  branch: sourceBranch,
3979
- targetBranch: "develop",
4331
+ targetBranch,
3980
4332
  created: true,
3981
- pullRequest: mapPullRequest(context, repo, data)
4333
+ pullRequest
3982
4334
  };
4335
+ if (options.workItemIds && options.workItemIds.length > 0) {
4336
+ result.workItems = await linkWorkItems(context, repo, cred, pullRequest.id, options.workItemIds);
4337
+ }
4338
+ return result;
3983
4339
  }
3984
4340
  async function updatePullRequest(context, repo, cred, prId, fields) {
3985
4341
  if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
@@ -4067,10 +4423,25 @@ async function updateThreadComment(context, repo, cred, prId, threadId, commentI
4067
4423
  return {
4068
4424
  id: data.id,
4069
4425
  author: data.author?.displayName ?? null,
4426
+ authorUniqueName: data.author?.uniqueName ?? null,
4427
+ authorId: data.author?.id ?? null,
4070
4428
  content: data.content ?? content,
4071
4429
  publishedAt: data.publishedDate ?? null
4072
4430
  };
4073
4431
  }
4432
+ async function deleteThreadComment(context, repo, cred, prId, threadId, commentId) {
4433
+ const url = new URL(
4434
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments/${commentId}`
4435
+ );
4436
+ url.searchParams.set("api-version", "7.1");
4437
+ const response = await fetchWithErrors(url.toString(), {
4438
+ method: "DELETE",
4439
+ headers: authHeaders(cred)
4440
+ });
4441
+ if (!response.ok) {
4442
+ throw httpError(response);
4443
+ }
4444
+ }
4074
4445
  async function postThreadComment(context, repo, cred, prId, threadId, content) {
4075
4446
  const response = await fetchWithErrors(buildThreadCommentUrl(context, repo, prId, threadId).toString(), {
4076
4447
  method: "POST",
@@ -4084,6 +4455,8 @@ async function postThreadComment(context, repo, cred, prId, threadId, content) {
4084
4455
  return {
4085
4456
  id: data.id,
4086
4457
  author: data.author?.displayName ?? null,
4458
+ authorUniqueName: data.author?.uniqueName ?? null,
4459
+ authorId: data.author?.id ?? null,
4087
4460
  content: data.content ?? content,
4088
4461
  publishedAt: data.publishedDate ?? null
4089
4462
  };
@@ -4200,13 +4573,34 @@ function buildPullRequestReviewerUrl(context, repo, prId, reviewerId) {
4200
4573
  url.searchParams.set("api-version", "7.1");
4201
4574
  return url;
4202
4575
  }
4576
+ function reviewerVoteState(vote) {
4577
+ switch (vote) {
4578
+ case 10:
4579
+ return "approved";
4580
+ case 5:
4581
+ return "approved-with-suggestions";
4582
+ case 0:
4583
+ return "no-vote";
4584
+ case -5:
4585
+ return "waiting-for-author";
4586
+ case -10:
4587
+ return "rejected";
4588
+ case 15:
4589
+ return "bypassed";
4590
+ default:
4591
+ return "unknown";
4592
+ }
4593
+ }
4203
4594
  function mapReviewer(data) {
4595
+ const vote = data.vote ?? 0;
4204
4596
  return {
4205
4597
  id: data.id,
4206
4598
  displayName: data.displayName ?? null,
4207
4599
  uniqueName: data.uniqueName ?? null,
4208
4600
  isRequired: data.isRequired ?? false,
4209
- vote: data.vote ?? 0
4601
+ vote,
4602
+ voteState: reviewerVoteState(vote),
4603
+ hasDeclined: data.hasDeclined ?? false
4210
4604
  };
4211
4605
  }
4212
4606
  async function getPullRequestReviewers(context, repo, cred, prId) {
@@ -4258,11 +4652,11 @@ function branchTemplateSegments(branch) {
4258
4652
  }
4259
4653
  return segments;
4260
4654
  }
4261
- async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
4655
+ async function fetchRepositoryItemContent(context, repo, cred, path5, branch) {
4262
4656
  const url = new URL(
4263
4657
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/items`
4264
4658
  );
4265
- url.searchParams.set("path", path3);
4659
+ url.searchParams.set("path", path5);
4266
4660
  url.searchParams.set("versionDescriptor.version", branch);
4267
4661
  url.searchParams.set("versionDescriptor.versionType", "branch");
4268
4662
  url.searchParams.set("includeContent", "true");
@@ -4305,10 +4699,10 @@ function defaultTemplateCandidates() {
4305
4699
  return paths;
4306
4700
  }
4307
4701
  async function firstMatchingTemplate(context, repo, cred, defaultBranch, candidates, kind) {
4308
- for (const path3 of candidates) {
4309
- const content = await fetchRepositoryItemContent(context, repo, cred, path3, defaultBranch);
4702
+ for (const path5 of candidates) {
4703
+ const content = await fetchRepositoryItemContent(context, repo, cred, path5, defaultBranch);
4310
4704
  if (content !== null) {
4311
- return { path: path3, content, kind };
4705
+ return { path: path5, content, kind };
4312
4706
  }
4313
4707
  }
4314
4708
  return null;
@@ -4334,7 +4728,7 @@ function parsePositivePrNumber(raw) {
4334
4728
  return null;
4335
4729
  }
4336
4730
  const n = Number.parseInt(raw, 10);
4337
- return Number.isFinite(n) && n > 0 ? n : null;
4731
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
4338
4732
  }
4339
4733
  function prNumberHelp(branchStatus = "active") {
4340
4734
  const matched = branchStatus === "abandoned" ? "abandoned" : "open";
@@ -4372,24 +4766,11 @@ function writeContractError(line) {
4372
4766
  process.exitCode = 1;
4373
4767
  }
4374
4768
  var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
4375
- var STDIN_PATH = "-";
4376
- function readTextSource(file) {
4377
- if (file === STDIN_PATH) {
4378
- try {
4379
- return readFileSync5(0, "utf-8");
4380
- } catch {
4381
- writeError("Cannot read standard input.");
4382
- return null;
4383
- }
4384
- }
4385
- if (!existsSync5(file)) {
4386
- writeError(`File not found: ${file}`);
4387
- return null;
4388
- }
4769
+ function readTextSource2(file) {
4389
4770
  try {
4390
- return readFileSync5(file, "utf-8");
4391
- } catch {
4392
- writeError(`Cannot read file: ${file}`);
4771
+ return readTextSource(file);
4772
+ } catch (err) {
4773
+ writeError(err.message);
4393
4774
  return null;
4394
4775
  }
4395
4776
  }
@@ -4400,7 +4781,7 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4400
4781
  }
4401
4782
  let body;
4402
4783
  if (file !== void 0) {
4403
- const read = readTextSource(file);
4784
+ const read = readTextSource2(file);
4404
4785
  if (read === null) {
4405
4786
  return null;
4406
4787
  }
@@ -4430,7 +4811,7 @@ function resolveOptionalTextInput(inline, file, flag) {
4430
4811
  const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
4431
4812
  let value;
4432
4813
  if (file !== void 0) {
4433
- const read = readTextSource(file);
4814
+ const read = readTextSource2(file);
4434
4815
  if (read === null) {
4435
4816
  return null;
4436
4817
  }
@@ -4644,24 +5025,67 @@ async function resolvePrCommandContext(options, resolveOpts = {}) {
4644
5025
  pat: credential
4645
5026
  };
4646
5027
  }
4647
- function createPrStatusCommand() {
4648
- const command = new Command12("status");
4649
- withCommonPrOptions(command).description("Check pull requests for the current branch").option("--json", "output JSON").action(async (options) => {
4650
- validateOrgProjectPair(options);
4651
- let context;
4652
- try {
4653
- const resolved = await resolvePrCommandContext(options);
4654
- context = resolved.context;
4655
- const branch = resolved.branch;
4656
- const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
4657
- let projectId = null;
4658
- try {
4659
- projectId = await resolveProjectId(resolved.context, resolved.pat);
4660
- } catch {
4661
- projectId = null;
4662
- }
4663
- const pullRequestsWithChecks = await Promise.all(
4664
- pullRequests.map(
5028
+ var STATUS_PR_NUMBER_HELP = "show only the pull request with this numeric id, instead of the current branch's pull requests; mutually exclusive with --branch";
5029
+ var STATUS_BRANCH_HELP = "show the pull requests whose source branch is this one (with or without the refs/heads/ prefix), instead of the current branch's; no checkout needed. Mutually exclusive with --pr-number";
5030
+ function parseStatusTarget(options) {
5031
+ if (options.prNumber !== void 0 && options.branch !== void 0) {
5032
+ writeError("Cannot specify both --pr-number and --branch.");
5033
+ return null;
5034
+ }
5035
+ if (options.branch !== void 0) {
5036
+ const branch = options.branch.trim().replace(/^refs\/heads\//, "");
5037
+ if (branch === "") {
5038
+ writeError("--branch must not be empty.");
5039
+ return null;
5040
+ }
5041
+ return { kind: "branch", branch };
5042
+ }
5043
+ const prId = parseTargetPrNumber(options);
5044
+ if (prId === "invalid") {
5045
+ return null;
5046
+ }
5047
+ return prId === "none" ? { kind: "current" } : { kind: "pr", prId };
5048
+ }
5049
+ async function findStatusPullRequests(resolved, target) {
5050
+ if (target.kind === "pr") {
5051
+ const pullRequest = await fetchTargetById(resolved, target.prId);
5052
+ return pullRequest === null ? null : { branch: formatBranchName(pullRequest.sourceRefName), pullRequests: [pullRequest] };
5053
+ }
5054
+ const branch = target.kind === "branch" ? target.branch : resolved.branch;
5055
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
5056
+ if (target.kind === "branch" && pullRequests.length === 0) {
5057
+ writeError(
5058
+ `No pull requests found for branch ${branch} in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`
5059
+ );
5060
+ return null;
5061
+ }
5062
+ return { branch, pullRequests };
5063
+ }
5064
+ function createPrStatusCommand() {
5065
+ const command = new Command13("status");
5066
+ withCommonPrOptions(configureUnwrappedHelp(command)).description("Check pull requests for the current branch, another branch, or a specific pull request").option("--pr-number <id>", STATUS_PR_NUMBER_HELP).option("--branch <name>", STATUS_BRANCH_HELP).option("--json", "output JSON").action(async (options) => {
5067
+ validateOrgProjectPair(options);
5068
+ const target = parseStatusTarget(options);
5069
+ if (target === null) {
5070
+ return;
5071
+ }
5072
+ let context;
5073
+ try {
5074
+ const resolved = await resolvePrCommandContext(options, { requireBranch: target.kind === "current" });
5075
+ context = resolved.context;
5076
+ const found = await findStatusPullRequests(resolved, target);
5077
+ if (found === null) {
5078
+ return;
5079
+ }
5080
+ const { branch, pullRequests } = found;
5081
+ let projectId = null;
5082
+ try {
5083
+ projectId = await resolveProjectId(resolved.context, resolved.pat);
5084
+ } catch {
5085
+ projectId = null;
5086
+ }
5087
+ const pullRequestsWithChecks = await Promise.all(
5088
+ pullRequests.map(
4665
5089
  async (pullRequest) => buildPullRequestStatusEntry(resolved.context, resolved.repo, resolved.pat, pullRequest, projectId)
4666
5090
  )
4667
5091
  );
@@ -4684,11 +5108,11 @@ function createPrStatusCommand() {
4684
5108
  });
4685
5109
  return command;
4686
5110
  }
4687
- function handlePrOpenError(err, context) {
5111
+ function handlePrOpenError(err, context, targetBranch = "develop") {
4688
5112
  const message = err instanceof Error ? err.message : "";
4689
5113
  if (message.startsWith("AMBIGUOUS_PRS:")) {
4690
5114
  const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
4691
- writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
5115
+ writeError(`Multiple active pull requests already exist for this branch targeting ${targetBranch}: ${ids}. Use pr status to review them.`);
4692
5116
  return;
4693
5117
  }
4694
5118
  if (message === "DESCRIPTION_REQUIRED") {
@@ -4701,13 +5125,54 @@ function handlePrOpenError(err, context) {
4701
5125
  }
4702
5126
  handlePrCommandError(err, context, "write");
4703
5127
  }
5128
+ function collectOption(value, previous) {
5129
+ return [...previous, value];
5130
+ }
5131
+ function stripHeadsPrefix(branch) {
5132
+ return branch.trim().replace(/^refs\/heads\//, "");
5133
+ }
5134
+ function resolveOpenOptions(options) {
5135
+ const targetBranch = options.target === void 0 ? "develop" : stripHeadsPrefix(options.target);
5136
+ const source = options.source === void 0 ? void 0 : stripHeadsPrefix(options.source);
5137
+ if (!targetBranch) {
5138
+ writeError("--target must not be empty.");
5139
+ return null;
5140
+ }
5141
+ if (source !== void 0 && !source) {
5142
+ writeError("--source must not be empty.");
5143
+ return null;
5144
+ }
5145
+ const workItemIds = [];
5146
+ for (const raw of options.workItem ?? []) {
5147
+ const id = Number(raw.trim());
5148
+ if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(id) || id <= 0) {
5149
+ writeError(`--work-item must be a positive integer, got "${raw}".`);
5150
+ return null;
5151
+ }
5152
+ if (!workItemIds.includes(id)) {
5153
+ workItemIds.push(id);
5154
+ }
5155
+ }
5156
+ const labels = [];
5157
+ for (const raw of options.label ?? []) {
5158
+ const name = raw.trim();
5159
+ if (!name) {
5160
+ writeError("--label must not be empty.");
5161
+ return null;
5162
+ }
5163
+ if (!labels.includes(name)) {
5164
+ labels.push(name);
5165
+ }
5166
+ }
5167
+ return { targetBranch, source, isDraft: options.draft === true, labels, workItemIds };
5168
+ }
4704
5169
  function resolveOpenDescription(inline, file) {
4705
5170
  if (inline !== void 0 && file !== void 0) {
4706
5171
  writeError("Cannot specify both --description and --description-file.");
4707
5172
  return null;
4708
5173
  }
4709
5174
  if (file !== void 0) {
4710
- const fromFile = readTextSource(file);
5175
+ const fromFile = readTextSource2(file);
4711
5176
  if (fromFile === null) {
4712
5177
  return null;
4713
5178
  }
@@ -4725,14 +5190,19 @@ function resolveOpenDescription(inline, file) {
4725
5190
  return trimmedInline;
4726
5191
  }
4727
5192
  function createPrOpenCommand() {
4728
- const command = new Command12("open");
4729
- withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
5193
+ const command = new Command13("open");
5194
+ withCommonPrOptions(command).description("Open a pull request (default: from the current branch to develop)").option("--title <title>", "pull request title").option(
4730
5195
  "--description <description>",
4731
5196
  "pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
4732
5197
  ).option(
4733
5198
  "--description-file <path>",
4734
5199
  'read the description from a UTF-8 file instead of --description; "-" reads standard input'
4735
- ).option("--json", "output JSON").action(async (options) => {
5200
+ ).option("--target <branch>", "target branch (default: develop); refs/heads/ prefix accepted").option("--source <branch>", "source branch (default: the current branch); no local checkout needed").option("--draft", "open the pull request as a draft").option(
5201
+ "--work-item <id>",
5202
+ "link a work item to the pull request (positive integer; repeatable)",
5203
+ collectOption,
5204
+ []
5205
+ ).option("--label <label>", "add a label to the pull request (repeatable)", collectOption, []).option("--json", "output JSON; adds top-level id and url next to pullRequest").action(async (options) => {
4736
5206
  validateOrgProjectPair(options);
4737
5207
  const title = options.title?.trim();
4738
5208
  if (!title) {
@@ -4743,45 +5213,71 @@ function createPrOpenCommand() {
4743
5213
  if (description === null) {
4744
5214
  return;
4745
5215
  }
5216
+ const openOptions = resolveOpenOptions(options);
5217
+ if (openOptions === null) {
5218
+ return;
5219
+ }
4746
5220
  let context;
4747
5221
  try {
4748
- const resolved = await resolvePrCommandContext(options);
4749
- context = resolved.context;
4750
- if (resolved.branch === "develop") {
4751
- writeError("Pull request creation requires a source branch other than develop.");
5222
+ const openBranch = openOptions.source ?? getCurrentBranch();
5223
+ const target = openOptions.targetBranch;
5224
+ if (openBranch === target) {
5225
+ writeError(
5226
+ target === "develop" ? "Pull request creation requires a source branch other than develop." : `Pull request creation requires a source branch other than the target branch (${target}).`
5227
+ );
4752
5228
  return;
4753
5229
  }
4754
- const openBranch = resolved.branch;
5230
+ const resolved = await resolvePrCommandContext(options, { requireBranch: false });
5231
+ context = resolved.context;
4755
5232
  const result = await openPullRequest(
4756
5233
  resolved.context,
4757
5234
  resolved.repo,
4758
5235
  resolved.pat,
4759
5236
  openBranch,
4760
5237
  title,
4761
- description
5238
+ description,
5239
+ openOptions
4762
5240
  );
4763
5241
  if (options.json) {
4764
- process.stdout.write(`${JSON.stringify(result, null, 2)}
5242
+ const output = { ...result, id: result.pullRequest.id, url: result.pullRequest.url };
5243
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
4765
5244
  `);
5245
+ reportWorkItemLinkFailures(result);
4766
5246
  return;
4767
5247
  }
4768
5248
  if (result.created) {
4769
5249
  process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
4770
5250
  ${result.pullRequest.url ?? "\u2014"}
4771
5251
  `);
5252
+ reportWorkItemLinkFailures(result);
4772
5253
  return;
4773
5254
  }
4774
5255
  process.stdout.write(
4775
- `Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
5256
+ `Active pull request already exists for ${openBranch} -> ${result.targetBranch}: #${result.pullRequest.id}
4776
5257
  ${result.pullRequest.url ?? "\u2014"}
4777
5258
  `
4778
5259
  );
5260
+ if (openOptions.isDraft || openOptions.labels?.length || openOptions.workItemIds?.length) {
5261
+ const hint = openOptions.workItemIds?.length ? " Use pr work-items link to link a work item." : "";
5262
+ process.stdout.write(
5263
+ `--draft, --label and --work-item were not applied; the existing pull request was left unchanged.${hint}
5264
+ `
5265
+ );
5266
+ }
4779
5267
  } catch (err) {
4780
- handlePrOpenError(err, context);
5268
+ handlePrOpenError(err, context, openOptions.targetBranch);
4781
5269
  }
4782
5270
  });
4783
5271
  return command;
4784
5272
  }
5273
+ function reportWorkItemLinkFailures(result) {
5274
+ const failed = (result.workItems ?? []).filter((item) => !item.linked);
5275
+ if (failed.length === 0) return;
5276
+ const detail = failed.map((item) => `#${item.id} (${item.error ?? "unknown error"})`).join(", ");
5277
+ writeError(
5278
+ `Pull request #${result.pullRequest.id} was created but could not be linked to work item ${detail}. Retry with: azdo pr work-items link <id> --pr-number ${result.pullRequest.id}`
5279
+ );
5280
+ }
4785
5281
  function handlePrUpdateError(err, context) {
4786
5282
  const message = err instanceof Error ? err.message : "";
4787
5283
  if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
@@ -4893,7 +5389,7 @@ ${updated.url ?? "\u2014"}
4893
5389
  }
4894
5390
  }
4895
5391
  function createPrUpdateCommand() {
4896
- const command = new Command12("update");
5392
+ const command = new Command13("update");
4897
5393
  withCommonPrOptions(configureUnwrappedHelp(command)).alias("edit").description("Update a pull request's title and/or description").option("--pr-number <N>", PR_NUMBER_HELP).option("--title <title>", "new pull request title").option("--title-file <path>", 'read the new title from a UTF-8 file; "-" reads standard input').option(
4898
5394
  "--description <description>",
4899
5395
  'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
@@ -4981,7 +5477,7 @@ ${updated.url ?? "\u2014"}
4981
5477
  }
4982
5478
  }
4983
5479
  function createPrAbandonCommand() {
4984
- const command = new Command12("abandon");
5480
+ const command = new Command13("abandon");
4985
5481
  withCommonPrOptions(configureUnwrappedHelp(command)).alias("close").description(
4986
5482
  'Abandon a pull request (status: abandoned) \u2014 reversible with "azdo pr reactivate". This does NOT complete or merge the pull request; nothing is deleted and no confirmation is asked.'
4987
5483
  ).option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
@@ -4990,7 +5486,7 @@ function createPrAbandonCommand() {
4990
5486
  return command;
4991
5487
  }
4992
5488
  function createPrReactivateCommand() {
4993
- const command = new Command12("reactivate");
5489
+ const command = new Command13("reactivate");
4994
5490
  withCommonPrOptions(configureUnwrappedHelp(command)).description(
4995
5491
  "Reactivate an abandoned pull request (status: active). When --pr-number is omitted the current branch's single ABANDONED pull request is used, not the active one."
4996
5492
  ).option("--pr-number <N>", PR_NUMBER_HELP_ABANDONED).option("--json", "output JSON").action(async (_options, command2) => {
@@ -5096,7 +5592,7 @@ function describeEmptyThreads(pullRequestId, allThreadCount, active) {
5096
5592
  `;
5097
5593
  }
5098
5594
  function createPrCommentsCommand() {
5099
- const command = new Command12("comments");
5595
+ const command = new Command13("comments");
5100
5596
  withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull request comment threads for the current branch").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--exclude-resolved", "alias of --hide-resolved: exclude resolved / won't fix / closed / by design threads").option("--code-related-only", "show only threads anchored to a file/line; omit general discussion threads").option("--exclude-system", "omit Azure DevOps system comments (branch updates, reviewer votes, build events)").option("--max-chars <N>", "truncate each comment body to N characters (0 = no limit, the default)").option("--thread <id>", "show only the thread with this numeric id; fails when the pull request has no such thread").option("--contains <text>", "show only threads holding a comment that contains this literal, case-sensitive substring (matched before --max-chars truncates)").option("--json", "output JSON").action(async (options) => {
5101
5597
  validateOrgProjectPair(options);
5102
5598
  const parsed = parseCommentsOptions(options);
@@ -5152,6 +5648,7 @@ function createPrCommentsCommand() {
5152
5648
  command.addCommand(createPrCommentsReplyCommand());
5153
5649
  command.addCommand(createPrCommentsAddCommand());
5154
5650
  command.addCommand(createPrCommentsEditCommand());
5651
+ command.addCommand(createPrCommentsDeleteCommand());
5155
5652
  return command;
5156
5653
  }
5157
5654
  function parseTargetPrNumber(options) {
@@ -5291,14 +5788,14 @@ async function runThreadStateChange(threadIdRaw, options, direction) {
5291
5788
  }
5292
5789
  }
5293
5790
  function createPrCommentResolveCommand() {
5294
- const command = new Command12("comment-resolve");
5791
+ const command = new Command13("comment-resolve");
5295
5792
  withCommonPrOptions(configureUnwrappedHelp(command)).description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
5296
5793
  await runThreadStateChange(threadIdRaw, options, "resolve");
5297
5794
  });
5298
5795
  return command;
5299
5796
  }
5300
5797
  function createPrCommentReopenCommand() {
5301
- const command = new Command12("comment-reopen");
5798
+ const command = new Command13("comment-reopen");
5302
5799
  withCommonPrOptions(configureUnwrappedHelp(command)).description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
5303
5800
  await runThreadStateChange(threadIdRaw, options, "reopen");
5304
5801
  });
@@ -5350,7 +5847,7 @@ async function runCommentReply(threadIdRaw, text, options) {
5350
5847
  }
5351
5848
  }
5352
5849
  function buildCommentReplyCommand(name, description) {
5353
- const command = new Command12(name);
5850
+ const command = new Command13(name);
5354
5851
  withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread to reply to").argument("[text]", "text of the reply; omit when using --file").option("--file <path>", "read the reply body from a UTF-8 file instead of the inline argument").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, _options, command2) => {
5355
5852
  await runCommentReply(threadIdRaw, text, mergedPrOptions(command2));
5356
5853
  });
@@ -5452,7 +5949,7 @@ ${body}
5452
5949
  }
5453
5950
  }
5454
5951
  function buildCommentAddCommand(name, description) {
5455
- const command = new Command12(name);
5952
+ const command = new Command13(name);
5456
5953
  withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("[text]", "body of the new comment; omit when using --file").option("--file <path>", "read the comment body from a UTF-8 file instead of the inline argument").option(
5457
5954
  "--status <status>",
5458
5955
  `thread status (${CREATABLE_THREAD_STATUSES.join(" | ")}); omit for a plain, non-resolvable comment`
@@ -5470,7 +5967,26 @@ function createPrCommentAddCommand() {
5470
5967
  'Post a new comment thread on the pull request overview (alias of "azdo pr comments add")'
5471
5968
  );
5472
5969
  }
5473
- async function fetchThreadForEdit(target) {
5970
+ function parseExplicitCommentId(options) {
5971
+ if (options.commentId === void 0) {
5972
+ return null;
5973
+ }
5974
+ const parsed = parsePositivePrNumber(options.commentId);
5975
+ if (parsed === null) {
5976
+ writeError(`Invalid --comment-id "${options.commentId}"; expected a positive integer.`);
5977
+ return "invalid";
5978
+ }
5979
+ return parsed;
5980
+ }
5981
+ function findCommentInThread(thread, commentId, target) {
5982
+ const match = thread.comments.find((comment) => comment.id === commentId);
5983
+ if (match === void 0) {
5984
+ writeError(`Comment #${commentId} not found in thread #${target.threadId} on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
5985
+ return null;
5986
+ }
5987
+ return match;
5988
+ }
5989
+ async function fetchTargetThread(target) {
5474
5990
  try {
5475
5991
  return await getPullRequestThread(
5476
5992
  target.context,
@@ -5489,12 +6005,7 @@ async function fetchThreadForEdit(target) {
5489
6005
  }
5490
6006
  function selectEditableComment(thread, explicitCommentId, target) {
5491
6007
  if (explicitCommentId !== null) {
5492
- const match = thread.comments.find((comment) => comment.id === explicitCommentId);
5493
- if (match === void 0) {
5494
- writeError(`Comment #${explicitCommentId} not found in thread #${target.threadId} on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
5495
- return null;
5496
- }
5497
- return match;
6008
+ return findCommentInThread(thread, explicitCommentId, target);
5498
6009
  }
5499
6010
  const first = [...thread.comments].sort((a, b) => a.id - b.id)[0];
5500
6011
  if (first === void 0) {
@@ -5529,13 +6040,9 @@ async function runCommentEdit(threadIdRaw, text, options) {
5529
6040
  if (body === null) {
5530
6041
  return;
5531
6042
  }
5532
- let explicitCommentId = null;
5533
- if (options.commentId !== void 0) {
5534
- explicitCommentId = parsePositivePrNumber(options.commentId);
5535
- if (explicitCommentId === null) {
5536
- writeError(`Invalid --comment-id "${options.commentId}"; expected a positive integer.`);
5537
- return;
5538
- }
6043
+ const explicitCommentId = parseExplicitCommentId(options);
6044
+ if (explicitCommentId === "invalid") {
6045
+ return;
5539
6046
  }
5540
6047
  const target = await resolveThreadTarget(threadIdRaw, options, {
5541
6048
  onContextResolved: (resolved) => {
@@ -5545,7 +6052,7 @@ async function runCommentEdit(threadIdRaw, text, options) {
5545
6052
  if (target === null) {
5546
6053
  return;
5547
6054
  }
5548
- const thread = await fetchThreadForEdit(target);
6055
+ const thread = await fetchTargetThread(target);
5549
6056
  if (thread === null) {
5550
6057
  return;
5551
6058
  }
@@ -5592,7 +6099,7 @@ async function runCommentEdit(threadIdRaw, text, options) {
5592
6099
  }
5593
6100
  }
5594
6101
  function buildCommentEditCommand(name, description) {
5595
- const command = new Command12(name);
6102
+ const command = new Command13(name);
5596
6103
  withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread holding the comment").argument("[text]", "new comment body; omit when using --file").option("--comment-id <N>", "numeric id of the comment to edit; defaults to the thread's first comment").option("--file <path>", "read the new body from a UTF-8 file instead of the inline argument").option("--dry-run", "print the replacement body plus the current/new lengths, without writing anything (--json also returns previousContent)").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, _options, command2) => {
5597
6104
  await runCommentEdit(threadIdRaw, text, mergedPrOptions(command2));
5598
6105
  });
@@ -5607,44 +6114,176 @@ function createPrCommentEditCommand() {
5607
6114
  'Edit an existing pull request comment in place (alias of "azdo pr comments edit")'
5608
6115
  );
5609
6116
  }
6117
+ function selectDeletableComment(thread, explicitCommentId, target) {
6118
+ if (explicitCommentId !== null) {
6119
+ return findCommentInThread(thread, explicitCommentId, target);
6120
+ }
6121
+ const comments = [...thread.comments].sort((a, b) => a.id - b.id);
6122
+ if (comments.length === 1) {
6123
+ return comments[0];
6124
+ }
6125
+ if (comments.length === 0) {
6126
+ writeError(`Thread #${target.threadId} on pull request #${target.pullRequest.id} has no comment to delete.`, EXIT_NOT_FOUND);
6127
+ return null;
6128
+ }
6129
+ const candidates = comments.map((comment) => `#${comment.id} (${comment.author ?? "Unknown"})`).join(", ");
6130
+ writeError(
6131
+ `Thread #${target.threadId} on pull request #${target.pullRequest.id} holds ${comments.length} comments; pass --comment-id to choose one: ${candidates}.`
6132
+ );
6133
+ return null;
6134
+ }
6135
+ function reportDeleteResult(result, json, comment) {
6136
+ if (json) {
6137
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
6138
+ `);
6139
+ return;
6140
+ }
6141
+ if (result.dryRun) {
6142
+ process.stdout.write(
6143
+ `Dry run: would delete comment #${result.commentId} by ${comment.author ?? "Unknown"} (${comment.content.length} chars) from thread #${result.threadId} on pull request #${result.pullRequestId}.
6144
+ `
6145
+ );
6146
+ return;
6147
+ }
6148
+ process.stdout.write(
6149
+ `Comment #${result.commentId} deleted from thread #${result.threadId} on pull request #${result.pullRequestId}.
6150
+ `
6151
+ );
6152
+ }
6153
+ async function runCommentDelete(threadIdRaw, options) {
6154
+ let context;
6155
+ try {
6156
+ const explicitCommentId = parseExplicitCommentId(options);
6157
+ if (explicitCommentId === "invalid") {
6158
+ return;
6159
+ }
6160
+ const target = await resolveThreadTarget(threadIdRaw, options, {
6161
+ onContextResolved: (resolved) => {
6162
+ context = resolved;
6163
+ }
6164
+ });
6165
+ if (target === null) {
6166
+ return;
6167
+ }
6168
+ const thread = await fetchTargetThread(target);
6169
+ if (thread === null) {
6170
+ return;
6171
+ }
6172
+ const comment = selectDeletableComment(thread, explicitCommentId, target);
6173
+ if (comment === null) {
6174
+ return;
6175
+ }
6176
+ const dryRun = options.dryRun === true;
6177
+ if (!dryRun) {
6178
+ await deleteThreadComment(
6179
+ target.context,
6180
+ target.repo,
6181
+ target.pat,
6182
+ target.pullRequest.id,
6183
+ target.threadId,
6184
+ comment.id
6185
+ );
6186
+ }
6187
+ reportDeleteResult(
6188
+ {
6189
+ pullRequestId: target.pullRequest.id,
6190
+ threadId: target.threadId,
6191
+ commentId: comment.id,
6192
+ deleted: !dryRun,
6193
+ dryRun
6194
+ },
6195
+ options.json === true,
6196
+ comment
6197
+ );
6198
+ } catch (err) {
6199
+ handlePrCommandError(err, context, "write");
6200
+ }
6201
+ }
6202
+ function buildCommentDeleteCommand(name, description) {
6203
+ const command = new Command13(name);
6204
+ withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread holding the comment").option("--comment-id <N>", "numeric id of the comment to delete; may be omitted only when the thread holds a single comment").option("--dry-run", "resolve the target comment and print what would be deleted, without deleting anything").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, _options, command2) => {
6205
+ await runCommentDelete(threadIdRaw, mergedPrOptions(command2));
6206
+ });
6207
+ return command;
6208
+ }
6209
+ function createPrCommentsDeleteCommand() {
6210
+ return buildCommentDeleteCommand("delete", "Delete a pull request comment (no confirmation prompt; irreversible)");
6211
+ }
6212
+ function createPrCommentDeleteCommand() {
6213
+ return buildCommentDeleteCommand(
6214
+ "comment-delete",
6215
+ 'Delete a pull request comment (alias of "azdo pr comments delete"; no confirmation prompt; irreversible)'
6216
+ );
6217
+ }
5610
6218
  var LIST_STATUS_VALUES = ["active", "completed", "abandoned", "all"];
5611
6219
  var DEFAULT_LIST_TOP = 25;
6220
+ var WORK_ITEM_LOOKUP_CONCURRENCY = 5;
5612
6221
  function formatPullRequestListEntry(pullRequest) {
5613
- return [
5614
- `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
6222
+ const state = pullRequest.isDraft ? `${pullRequest.status}, draft` : pullRequest.status;
6223
+ const lines = [
6224
+ `#${pullRequest.id} [${state}] ${pullRequest.title}`,
5615
6225
  ` ${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
5616
- ` Author: ${pullRequest.createdBy ?? "Unknown"}`,
5617
- ` ${pullRequest.url ?? "\u2014"}`
5618
- ].join("\n");
6226
+ ` Author: ${pullRequest.createdBy ?? "Unknown"}`
6227
+ ];
6228
+ if ("workItemIds" in pullRequest) {
6229
+ const ids = pullRequest.workItemIds.map((id) => `#${id}`).join(", ");
6230
+ lines.push(` Work items: ${ids || "none"}`);
6231
+ }
6232
+ lines.push(` ${pullRequest.url ?? "\u2014"}`);
6233
+ return lines.join("\n");
6234
+ }
6235
+ function parseListOptions(options) {
6236
+ const status2 = options.status ?? "active";
6237
+ if (!LIST_STATUS_VALUES.includes(status2)) {
6238
+ writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
6239
+ return null;
6240
+ }
6241
+ let top = DEFAULT_LIST_TOP;
6242
+ if (options.top !== void 0) {
6243
+ const parsed = parsePositivePrNumber(options.top);
6244
+ if (parsed === null) {
6245
+ writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
6246
+ return null;
6247
+ }
6248
+ top = parsed;
6249
+ }
6250
+ const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
6251
+ return { status: status2, top, branch };
6252
+ }
6253
+ async function attachWorkItemIds(context, repo, cred, pullRequests) {
6254
+ const result = new Array(pullRequests.length);
6255
+ let next = 0;
6256
+ const worker = async () => {
6257
+ while (next < pullRequests.length) {
6258
+ const index = next;
6259
+ next += 1;
6260
+ const pullRequest = pullRequests[index];
6261
+ const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
6262
+ result[index] = { ...pullRequest, workItemIds };
6263
+ }
6264
+ };
6265
+ await Promise.all(Array.from({ length: Math.min(WORK_ITEM_LOOKUP_CONCURRENCY, pullRequests.length) }, worker));
6266
+ return result;
5619
6267
  }
5620
6268
  function createPrListCommand() {
5621
- const command = new Command12("list");
5622
- 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) => {
6269
+ const command = new Command13("list");
6270
+ 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) => {
5623
6271
  validateOrgProjectPair(options);
5624
- const status2 = options.status ?? "active";
5625
- if (!LIST_STATUS_VALUES.includes(status2)) {
5626
- writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
6272
+ const parsed = parseListOptions(options);
6273
+ if (parsed === null) {
5627
6274
  return;
5628
6275
  }
5629
- let top = DEFAULT_LIST_TOP;
5630
- if (options.top !== void 0) {
5631
- const parsed = parsePositivePrNumber(options.top);
5632
- if (parsed === null) {
5633
- writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
5634
- return;
5635
- }
5636
- top = parsed;
5637
- }
5638
- const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
6276
+ const { status: status2, top, branch } = parsed;
5639
6277
  let context;
5640
6278
  try {
5641
6279
  const resolved = await resolvePrCommandContext(options, { requireBranch: false });
5642
6280
  context = resolved.context;
5643
- const pullRequests = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
6281
+ const listed = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
5644
6282
  sourceBranch: branch ?? void 0,
5645
6283
  status: status2,
5646
6284
  top
5647
6285
  });
6286
+ const pullRequests = options.workItems ? await attachWorkItemIds(resolved.context, resolved.repo, resolved.pat, listed) : listed;
5648
6287
  const result = {
5649
6288
  repository: resolved.repo,
5650
6289
  branch,
@@ -5725,14 +6364,14 @@ async function runWorkItemLinkChange(workItemIdRaw, options, direction) {
5725
6364
  }
5726
6365
  }
5727
6366
  function buildWorkItemLinkCommand(name, description, direction) {
5728
- const command = new Command12(name);
6367
+ const command = new Command13(name);
5729
6368
  withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<workItemId>", "numeric id of the work item").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (workItemIdRaw, _options, command2) => {
5730
6369
  await runWorkItemLinkChange(workItemIdRaw, mergedPrOptions(command2), direction);
5731
6370
  });
5732
6371
  return command;
5733
6372
  }
5734
6373
  function createPrWorkItemsCommand() {
5735
- const command = new Command12("work-items");
6374
+ const command = new Command13("work-items");
5736
6375
  command.description("Manage work items linked to a pull request");
5737
6376
  command.addCommand(buildWorkItemLinkCommand("link", "Link a work item to the pull request", "link"));
5738
6377
  command.addCommand(buildWorkItemLinkCommand("unlink", "Unlink a work item from the pull request", "unlink"));
@@ -5840,15 +6479,56 @@ async function runReviewerRemove(reviewer, options) {
5840
6479
  handlePrCommandError(err, context, "write");
5841
6480
  }
5842
6481
  }
6482
+ function formatReviewerLine(reviewer) {
6483
+ const name = reviewer.displayName ?? reviewer.uniqueName ?? reviewer.id;
6484
+ const unique = reviewer.uniqueName && reviewer.uniqueName !== name ? ` <${reviewer.uniqueName}>` : "";
6485
+ const kind = reviewer.isRequired ? "required" : "optional";
6486
+ const declined = reviewer.hasDeclined ? ", declined" : "";
6487
+ return `${name}${unique} \u2014 ${reviewer.voteState} (${kind}${declined})`;
6488
+ }
6489
+ async function runReviewerList(options) {
6490
+ let context;
6491
+ try {
6492
+ const target = await resolvePullRequestTarget(options, {
6493
+ onContextResolved: (resolved) => {
6494
+ context = resolved;
6495
+ }
6496
+ });
6497
+ if (target === null) {
6498
+ return;
6499
+ }
6500
+ const reviewers = await getPullRequestReviewers(target.context, target.repo, target.pat, target.pullRequest.id);
6501
+ const result = { pullRequestId: target.pullRequest.id, reviewers };
6502
+ if (options.json) {
6503
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
6504
+ `);
6505
+ return;
6506
+ }
6507
+ if (reviewers.length === 0) {
6508
+ process.stdout.write(`No reviewers on pull request #${target.pullRequest.id}.
6509
+ `);
6510
+ return;
6511
+ }
6512
+ process.stdout.write(`${reviewers.map(formatReviewerLine).join("\n")}
6513
+ `);
6514
+ } catch (err) {
6515
+ handlePrCommandError(err, context, "read");
6516
+ }
6517
+ }
5843
6518
  function createPrReviewersCommand() {
5844
- const command = new Command12("reviewers");
6519
+ const command = new Command13("reviewers");
5845
6520
  command.description("Manage pull request reviewers");
5846
- const add = new Command12("add");
6521
+ const list = new Command13("list");
6522
+ 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) => {
6523
+ await runReviewerList(mergedPrOptions(command2));
6524
+ });
6525
+ command.addCommand(list);
6526
+ const add = new Command13("add");
5847
6527
  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) => {
5848
6528
  await runReviewerAdd(reviewer, mergedPrOptions(command2));
5849
6529
  });
5850
6530
  command.addCommand(add);
5851
- const remove = new Command12("remove");
6531
+ const remove = new Command13("remove");
5852
6532
  withCommonPrOptions(configureUnwrappedHelp(remove)).description("Remove a reviewer from the pull request").argument("<reviewer>", "reviewer email or unique name").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
5853
6533
  await runReviewerRemove(reviewer, mergedPrOptions(command2));
5854
6534
  });
@@ -5856,7 +6536,7 @@ function createPrReviewersCommand() {
5856
6536
  return command;
5857
6537
  }
5858
6538
  function createPrCommand() {
5859
- const command = new Command12("pr");
6539
+ const command = new Command13("pr");
5860
6540
  command.description("Manage Azure DevOps pull requests");
5861
6541
  command.addCommand(createPrListCommand());
5862
6542
  command.addCommand(createPrStatusCommand());
@@ -5872,11 +6552,13 @@ function createPrCommand() {
5872
6552
  command.addCommand(createPrWorkItemsCommand());
5873
6553
  command.addCommand(createPrReviewersCommand());
5874
6554
  command.addCommand(createPrCommentEditCommand());
6555
+ command.addCommand(createPrCommentDeleteCommand());
5875
6556
  return command;
5876
6557
  }
5877
6558
 
5878
6559
  // src/commands/pipeline.ts
5879
- import { Command as Command13 } from "commander";
6560
+ import { Command as Command14 } from "commander";
6561
+ import path3 from "path";
5880
6562
 
5881
6563
  // src/services/pipeline-client.ts
5882
6564
  var API_VERSION = "7.1";
@@ -6000,6 +6682,9 @@ async function getBuildStatus(context, cred, buildId) {
6000
6682
  const build = await getBuild(context, cred, buildId);
6001
6683
  return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
6002
6684
  }
6685
+ function recordErrors(record) {
6686
+ return (record.issues ?? []).filter((issue) => issue.type === "error" && issue.message).map((issue) => ({ message: issue.message ?? "", source: record.name ?? null }));
6687
+ }
6003
6688
  async function getBuildTimeline(context, cred, buildId) {
6004
6689
  const url = withApiVersion(
6005
6690
  new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
@@ -6011,14 +6696,20 @@ async function getBuildTimeline(context, cred, buildId) {
6011
6696
  const stages = [];
6012
6697
  const jobs = [];
6013
6698
  const logSteps = /* @__PURE__ */ new Map();
6699
+ const logRecords = /* @__PURE__ */ new Map();
6700
+ const namesById = /* @__PURE__ */ new Map();
6014
6701
  for (const record of records) {
6015
- for (const issue of record.issues ?? []) {
6016
- if (issue.type === "error" && issue.message) {
6017
- errors.push({ message: issue.message, source: record.name ?? null });
6018
- }
6019
- }
6702
+ if (record.id && record.name) namesById.set(record.id, record.name);
6703
+ }
6704
+ for (const record of records) {
6705
+ errors.push(...recordErrors(record));
6020
6706
  if (record.name && record.log?.id !== void 0) {
6021
6707
  logSteps.set(record.log.id, record.name);
6708
+ logRecords.set(record.log.id, {
6709
+ name: record.name,
6710
+ type: record.type ?? null,
6711
+ parent: record.parentId && namesById.get(record.parentId) || null
6712
+ });
6022
6713
  }
6023
6714
  if (!record.name) continue;
6024
6715
  const status2 = {
@@ -6038,7 +6729,7 @@ async function getBuildTimeline(context, cred, buildId) {
6038
6729
  if (b.startTime === void 0) return -1;
6039
6730
  return a.startTime < b.startTime ? -1 : 1;
6040
6731
  });
6041
- return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
6732
+ return { errors, stages, jobs: jobs.map((j) => j.status), logSteps, logRecords };
6042
6733
  }
6043
6734
  async function listTestRuns(context, cred, buildId) {
6044
6735
  const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
@@ -6147,16 +6838,18 @@ async function getRunLogs(context, cred, buildId) {
6147
6838
  );
6148
6839
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6149
6840
  const data = await readJsonResponse2(response);
6150
- let logSteps = /* @__PURE__ */ new Map();
6841
+ let logRecords = /* @__PURE__ */ new Map();
6151
6842
  try {
6152
- logSteps = (await getBuildTimeline(context, cred, buildId)).logSteps;
6843
+ logRecords = (await getBuildTimeline(context, cred, buildId)).logRecords;
6153
6844
  } catch {
6154
6845
  }
6155
6846
  return data.value.map((log) => ({
6156
6847
  id: log.id,
6157
6848
  createdOn: log.createdOn ?? null,
6158
6849
  lineCount: log.lineCount ?? null,
6159
- step: logSteps.get(log.id) ?? null
6850
+ step: logRecords.get(log.id)?.name ?? null,
6851
+ type: logRecords.get(log.id)?.type ?? null,
6852
+ parent: logRecords.get(log.id)?.parent ?? null
6160
6853
  }));
6161
6854
  }
6162
6855
  async function getRunLog(context, cred, buildId, logId) {
@@ -6169,6 +6862,122 @@ async function getRunLog(context, cred, buildId, logId) {
6169
6862
  }
6170
6863
  return response.text();
6171
6864
  }
6865
+ async function listBuildArtifacts(context, cred, buildId) {
6866
+ const url = withApiVersion(
6867
+ new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/artifacts`)
6868
+ );
6869
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6870
+ const data = await readJsonResponse2(response);
6871
+ return data.value.map((artifact) => {
6872
+ const size = Number.parseInt(artifact.resource?.properties?.artifactsize ?? "", 10);
6873
+ return {
6874
+ id: artifact.id,
6875
+ name: artifact.name,
6876
+ type: artifact.resource?.type ?? null,
6877
+ sizeBytes: Number.isFinite(size) ? size : null,
6878
+ downloadUrl: artifact.resource?.downloadUrl ?? null
6879
+ };
6880
+ });
6881
+ }
6882
+ var INITIAL_BUFFER_BYTES = 8 * 1024 * 1024;
6883
+ async function collectBody(body, declared, onProgress, initialBytes = INITIAL_BUFFER_BYTES) {
6884
+ const hint = Number.isSafeInteger(declared) && declared > 0 ? declared : Infinity;
6885
+ let buffer = new Uint8Array(Math.min(hint, initialBytes));
6886
+ let received = 0;
6887
+ for await (const chunk of body) {
6888
+ const needed = received + chunk.length;
6889
+ if (needed > buffer.length) {
6890
+ let capacity = Math.max(buffer.length * 2, needed);
6891
+ if (hint >= needed) capacity = Math.min(capacity, hint);
6892
+ const grown = new Uint8Array(capacity);
6893
+ grown.set(buffer.subarray(0, received));
6894
+ buffer = grown;
6895
+ }
6896
+ buffer.set(chunk, received);
6897
+ received = needed;
6898
+ onProgress?.(received);
6899
+ }
6900
+ return buffer.subarray(0, received);
6901
+ }
6902
+ async function downloadArtifactZip(cred, artifact, onProgress) {
6903
+ if (!artifact.downloadUrl) {
6904
+ throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
6905
+ }
6906
+ const url = new URL(artifact.downloadUrl);
6907
+ url.searchParams.set("$format", "zip");
6908
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6909
+ if (!response.ok) {
6910
+ throw httpError(response);
6911
+ }
6912
+ if (!response.body) {
6913
+ return new Uint8Array(await response.arrayBuffer());
6914
+ }
6915
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
6916
+ return collectBody(response.body, declared, onProgress);
6917
+ }
6918
+
6919
+ // src/services/artifact-extract.ts
6920
+ import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
6921
+ import path2 from "path";
6922
+ import { unzipSync } from "fflate";
6923
+ function entryTarget(entry, artifactName) {
6924
+ const normalized = entry.replaceAll("\\", "/");
6925
+ if (normalized.endsWith("/")) return null;
6926
+ const prefix = `${artifactName}/`;
6927
+ const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
6928
+ const segments = relative.split("/");
6929
+ if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
6930
+ throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
6931
+ }
6932
+ return relative;
6933
+ }
6934
+ function assertNoSymlinkEscape(root, relative) {
6935
+ let current = root;
6936
+ for (const segment of relative.split("/")) {
6937
+ current = path2.join(current, segment);
6938
+ let isLink;
6939
+ try {
6940
+ isLink = lstatSync(current).isSymbolicLink();
6941
+ } catch {
6942
+ return;
6943
+ }
6944
+ if (isLink) {
6945
+ throw new Error(`Refusing to extract through symbolic link "${current}".`);
6946
+ }
6947
+ }
6948
+ }
6949
+ function extractArtifactZip(zip, destination, options) {
6950
+ const entries = unzipSync(zip);
6951
+ const targets = [];
6952
+ const seen = /* @__PURE__ */ new Set();
6953
+ for (const [entry, data] of Object.entries(entries)) {
6954
+ const relative = entryTarget(entry, options.artifactName);
6955
+ if (relative === null) continue;
6956
+ const key = path2.posix.normalize(relative);
6957
+ if (seen.has(key)) {
6958
+ throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
6959
+ }
6960
+ seen.add(key);
6961
+ targets.push({ relative: key, data });
6962
+ }
6963
+ if (!options.force) {
6964
+ const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
6965
+ if (existing) {
6966
+ throw new Error(
6967
+ `"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
6968
+ );
6969
+ }
6970
+ }
6971
+ mkdirSync(destination, { recursive: true });
6972
+ const root = realpathSync(destination);
6973
+ for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
6974
+ for (const { relative, data } of targets) {
6975
+ const file = path2.join(root, relative);
6976
+ mkdirSync(path2.dirname(file), { recursive: true });
6977
+ writeFileSync(file, data);
6978
+ }
6979
+ return targets.map((t) => t.relative);
6980
+ }
6172
6981
 
6173
6982
  // src/commands/pipeline.ts
6174
6983
  var EXIT_FAILED = 1;
@@ -6251,7 +7060,7 @@ function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
6251
7060
  ).join("\n");
6252
7061
  }
6253
7062
  function createPipelineListCommand() {
6254
- const command = new Command13("list");
7063
+ const command = new Command14("list");
6255
7064
  command.description("List Azure DevOps pipeline definitions").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--filter <name>", "filter definitions by name (case-insensitive substring)").option("--json", "output JSON").action(async (options) => {
6256
7065
  validateOrgProjectPair(options);
6257
7066
  let context;
@@ -6323,7 +7132,7 @@ function parseGetRunsInputs(defIdRaw, options) {
6323
7132
  return { defId, limit: limit ?? 10, prNumber };
6324
7133
  }
6325
7134
  function createPipelineGetRunsCommand() {
6326
- const command = new Command13("get-runs");
7135
+ const command = new Command14("get-runs");
6327
7136
  command.description("List recent runs for a pipeline definition (newest first)").argument("[def_id]", "pipeline definition id (optional with --commit or --pr)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--limit <n>", "maximum number of runs to show (default 10)").option("--branch <branch>", "only show runs for this source branch").option("--commit <sha>", "only show runs that built this commit (full or abbreviated SHA)").option("--pr <number>", "only show runs for this pull request").option("--json", "output JSON").action(async (defIdRaw, options) => {
6328
7137
  validateOrgProjectPair(options);
6329
7138
  const inputs = parseGetRunsInputs(defIdRaw, options);
@@ -6378,7 +7187,7 @@ function applyWaitExitCode(result) {
6378
7187
  }
6379
7188
  }
6380
7189
  function createPipelineWaitCommand() {
6381
- const command = new Command13("wait");
7190
+ const command = new Command14("wait");
6382
7191
  command.description("Wait for a pipeline run to finish; exit code reflects the result (0 success, non-zero otherwise)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--timeout <seconds>", "maximum seconds to wait (default 1800)").option("--poll-interval <seconds>", "seconds between status checks (default 5)").option("--json", "output JSON").action(async (runIdRaw, options) => {
6383
7192
  validateOrgProjectPair(options);
6384
7193
  const runId = parsePositiveId(runIdRaw);
@@ -6505,19 +7314,26 @@ function formatRunDetail(detail) {
6505
7314
  ...testRows(detail)
6506
7315
  ].join("\n");
6507
7316
  }
7317
+ async function withRunContext(runIdRaw, options, body) {
7318
+ validateOrgProjectPair(options);
7319
+ const runId = parsePositiveId(runIdRaw);
7320
+ if (runId === null) {
7321
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7322
+ return;
7323
+ }
7324
+ let context;
7325
+ try {
7326
+ const resolved = await resolvePipelineContext(options);
7327
+ context = resolved.context;
7328
+ await body(runId, resolved);
7329
+ } catch (err) {
7330
+ handlePipelineError(err, context);
7331
+ }
7332
+ }
6508
7333
  function createPipelineGetRunDetailCommand() {
6509
- const command = new Command13("get-run-detail");
6510
- command.description("Show a detailed summary of a single pipeline run (errors, failing tests, stages)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (runIdRaw, options) => {
6511
- validateOrgProjectPair(options);
6512
- const runId = parsePositiveId(runIdRaw);
6513
- if (runId === null) {
6514
- writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
6515
- return;
6516
- }
6517
- let context;
6518
- try {
6519
- const resolved = await resolvePipelineContext(options);
6520
- context = resolved.context;
7334
+ const command = new Command14("get-run-detail");
7335
+ command.description("Show a detailed summary of a single pipeline run (errors, failing tests, stages)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
7336
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
6521
7337
  const detail = await getRunDetail(resolved.context, resolved.cred, runId);
6522
7338
  if (options.json) {
6523
7339
  process.stdout.write(`${JSON.stringify(detail, null, 2)}
@@ -6526,10 +7342,8 @@ function createPipelineGetRunDetailCommand() {
6526
7342
  }
6527
7343
  process.stdout.write(`${formatRunDetail(detail)}
6528
7344
  `);
6529
- } catch (err) {
6530
- handlePipelineError(err, context);
6531
- }
6532
- });
7345
+ })
7346
+ );
6533
7347
  return command;
6534
7348
  }
6535
7349
  function grepWithContext(lines, grep, context) {
@@ -6552,17 +7366,37 @@ function grepWithContext(lines, grep, context) {
6552
7366
  }
6553
7367
  return selected;
6554
7368
  }
6555
- function filterLogLines(content, grep, tail, context) {
7369
+ var PROGRESS_BAR = /(?<![\d.])\d+(?:\.\d+)? [KMGT]?i?B \/ \d+(?:\.\d+)? [KMGT]?i?B \[/g;
7370
+ var LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z /;
7371
+ function collapseGluedBars(line) {
7372
+ const starts = [...line.matchAll(PROGRESS_BAR)].map((m) => m.index ?? 0);
7373
+ if (starts.length < 2) return line;
7374
+ const prefix = LOG_TIMESTAMP.exec(line)?.[0] ?? "";
7375
+ return prefix + line.slice(starts.at(-1));
7376
+ }
7377
+ function collapseProgress(line) {
7378
+ let end = line.length;
7379
+ while (end > 0 && line[end - 1] === "\r") end--;
7380
+ return collapseGluedBars(line.slice(line.lastIndexOf("\r", end - 1) + 1, end));
7381
+ }
7382
+ function filterLogLines(content, filters) {
7383
+ const { grep, tail, head, contextLines: context } = filters;
6556
7384
  let lines = content.split("\n");
6557
7385
  if (lines.at(-1) === "") {
6558
7386
  lines.pop();
6559
7387
  }
7388
+ if (filters.noProgress) {
7389
+ lines = lines.map(collapseProgress);
7390
+ }
6560
7391
  if (grep) {
6561
7392
  lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
6562
7393
  }
6563
7394
  if (tail !== void 0 && lines.length > tail) {
6564
7395
  lines = lines.slice(-tail);
6565
7396
  }
7397
+ if (head !== void 0 && lines.length > head) {
7398
+ lines = lines.slice(0, head);
7399
+ }
6566
7400
  return lines;
6567
7401
  }
6568
7402
  function parseLogFilters(options) {
@@ -6571,9 +7405,14 @@ function parseLogFilters(options) {
6571
7405
  return null;
6572
7406
  }
6573
7407
  const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
6574
- const slices = options.tail !== void 0 || options.grep !== void 0 || options.context !== void 0;
7408
+ const noProgress = options.progress === false;
7409
+ const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
6575
7410
  if (slices && !selectsSingleLog) {
6576
- writeError2("--tail, --grep, and --context require --log-id or --step.");
7411
+ writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
7412
+ return null;
7413
+ }
7414
+ if (options.head !== void 0 && options.tail !== void 0) {
7415
+ writeError2("Use either --head or --tail, not both.");
6577
7416
  return null;
6578
7417
  }
6579
7418
  if (options.context !== void 0 && options.grep === void 0) {
@@ -6582,6 +7421,8 @@ function parseLogFilters(options) {
6582
7421
  }
6583
7422
  const tail = parseOptionalCount(options.tail, "--tail");
6584
7423
  if (tail === null) return null;
7424
+ const head = parseOptionalCount(options.head, "--head");
7425
+ if (head === null) return null;
6585
7426
  const contextLines = parseOptionalCount(options.context, "--context");
6586
7427
  if (contextLines === null) return null;
6587
7428
  let grep;
@@ -6593,7 +7434,13 @@ function parseLogFilters(options) {
6593
7434
  return null;
6594
7435
  }
6595
7436
  }
6596
- return { tail, contextLines: contextLines ?? 0, grep };
7437
+ return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
7438
+ }
7439
+ function describeLog(log) {
7440
+ const kind = log.type ?? "";
7441
+ const where = log.parent ? ` in ${log.parent}` : "";
7442
+ const detail = `${kind}${where}`.trim();
7443
+ return detail ? `${log.step} [${detail}]` : String(log.step);
6597
7444
  }
6598
7445
  function chooseStepLog(logs, step, runId) {
6599
7446
  const needle = step.toLowerCase();
@@ -6605,7 +7452,7 @@ function chooseStepLog(logs, step, runId) {
6605
7452
  return null;
6606
7453
  }
6607
7454
  if (chosen.length > 1) {
6608
- const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
7455
+ const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
6609
7456
  writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
6610
7457
  return null;
6611
7458
  }
@@ -6627,8 +7474,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
6627
7474
  return chooseStepLog(allLogs, options.step, runId);
6628
7475
  }
6629
7476
  function printSingleLog(content, filters) {
6630
- if (filters.grep !== void 0 || filters.tail !== void 0) {
6631
- const lines = filterLogLines(content, filters.grep, filters.tail, filters.contextLines);
7477
+ if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
7478
+ const lines = filterLogLines(content, filters);
6632
7479
  if (lines.length > 0) {
6633
7480
  process.stdout.write(`${lines.join("\n")}
6634
7481
  `);
@@ -6639,8 +7486,8 @@ function printSingleLog(content, filters) {
6639
7486
  `);
6640
7487
  }
6641
7488
  function createPipelineLogsCommand() {
6642
- const command = new Command13("logs");
6643
- command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--json", "output JSON").action(async (runIdRaw, options) => {
7489
+ const command = new Command14("logs");
7490
+ command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--head <n>", "with --log-id/--step, print only the first N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--no-progress", "with --log-id/--step, collapse progress-bar redraws (carriage-return or glued into one line) to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
6644
7491
  validateOrgProjectPair(options);
6645
7492
  const runId = parsePositiveId(runIdRaw);
6646
7493
  if (runId === null) {
@@ -6679,7 +7526,9 @@ function createPipelineLogsCommand() {
6679
7526
  String(l.id),
6680
7527
  l.createdOn ?? "\u2014",
6681
7528
  l.lineCount == null ? "" : `${l.lineCount} lines`,
6682
- l.step ?? ""
7529
+ l.type ?? "",
7530
+ l.step ?? "",
7531
+ l.parent ? `(in ${l.parent})` : ""
6683
7532
  ]);
6684
7533
  process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
6685
7534
  `);
@@ -6689,8 +7538,104 @@ function createPipelineLogsCommand() {
6689
7538
  });
6690
7539
  return command;
6691
7540
  }
7541
+ function formatSize(bytes) {
7542
+ if (bytes === null) return "";
7543
+ if (bytes < 1024) return `${bytes} B`;
7544
+ const units = ["KB", "MB", "GB", "TB"];
7545
+ let value = bytes / 1024;
7546
+ let unit = 0;
7547
+ while (value >= 1024 && unit < units.length - 1) {
7548
+ value /= 1024;
7549
+ unit++;
7550
+ }
7551
+ return `${value.toFixed(1)} ${units[unit]}`;
7552
+ }
7553
+ function createPipelineArtifactsCommand() {
7554
+ const command = new Command14("artifacts");
7555
+ command.description("List the build artifacts published by a pipeline run").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
7556
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
7557
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7558
+ if (options.json) {
7559
+ process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
7560
+ `);
7561
+ return;
7562
+ }
7563
+ if (artifacts.length === 0) {
7564
+ process.stdout.write(`No artifacts found for run ${runId}.
7565
+ `);
7566
+ return;
7567
+ }
7568
+ const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
7569
+ process.stdout.write(`${formatTable(rows)}
7570
+ `);
7571
+ })
7572
+ );
7573
+ return command;
7574
+ }
7575
+ function selectArtifacts(artifacts, name, all, runId) {
7576
+ const available = artifacts.map((a) => a.name).join(", ") || "(none)";
7577
+ if (all) return artifacts;
7578
+ if (name === void 0) {
7579
+ writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
7580
+ return null;
7581
+ }
7582
+ const match = artifacts.find((a) => a.name === name);
7583
+ if (!match) {
7584
+ writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
7585
+ return null;
7586
+ }
7587
+ return [match];
7588
+ }
7589
+ function createPipelineArtifactDownloadCommand() {
7590
+ const command = new Command14("artifact-download");
7591
+ command.description("Download a run's artifact (or --all) and extract it into a folder; no zip is left behind").argument("<run_id>", "pipeline run id").argument("[name]", "artifact name (see `pipeline artifacts`)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--path <dir>", "destination folder (default ./<name>; with --all, <dir>/<name> per artifact)").option("--all", "download every artifact of the run").option("--force", "overwrite files that already exist").option("--progress", "print byte-progress lines on stderr").option("--json", "output JSON").action(async (runIdRaw, name, options) => {
7592
+ validateOrgProjectPair(options);
7593
+ const runId = parsePositiveId(runIdRaw);
7594
+ if (runId === null) {
7595
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7596
+ return;
7597
+ }
7598
+ if (options.all && name !== void 0) {
7599
+ writeError2("Use either an artifact name or --all, not both.");
7600
+ return;
7601
+ }
7602
+ let context;
7603
+ try {
7604
+ const resolved = await resolvePipelineContext(options);
7605
+ context = resolved.context;
7606
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7607
+ const selected = selectArtifacts(artifacts, name, options.all === true, runId);
7608
+ if (selected === null) return;
7609
+ const results = await Promise.all(
7610
+ selected.map(async (artifact) => {
7611
+ const destination = path3.resolve(
7612
+ options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
7613
+ );
7614
+ const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
7615
+ `) : void 0;
7616
+ const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
7617
+ const files = extractArtifactZip(zip, destination, {
7618
+ artifactName: artifact.name,
7619
+ force: options.force === true
7620
+ });
7621
+ return { name: artifact.name, path: destination, files: files.length };
7622
+ })
7623
+ );
7624
+ if (options.json) {
7625
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
7626
+ `);
7627
+ return;
7628
+ }
7629
+ process.stdout.write(`${results.map((r) => r.path).join("\n")}
7630
+ `);
7631
+ } catch (err) {
7632
+ handlePipelineError(err, context);
7633
+ }
7634
+ });
7635
+ return command;
7636
+ }
6692
7637
  function createPipelineTestsCommand() {
6693
- const command = new Command13("tests");
7638
+ const command = new Command14("tests");
6694
7639
  command.description("Show a run's test results: summary plus failing tests by name (no log grepping needed)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--failed", "list only the failing tests").option("--json", "output JSON").action(async (runIdRaw, options) => {
6695
7640
  validateOrgProjectPair(options);
6696
7641
  const runId = parsePositiveId(runIdRaw);
@@ -6747,7 +7692,7 @@ function collectParameter(value, previous) {
6747
7692
  return previous.concat([value]);
6748
7693
  }
6749
7694
  function createPipelineStartCommand() {
6750
- const command = new Command13("start");
7695
+ const command = new Command14("start");
6751
7696
  command.description("Queue a new run of a pipeline definition").argument("<def_id>", "pipeline definition id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--branch <branch>", "branch to run against (default: pipeline default branch)").option("--parameter <key=value>", "template parameter (repeatable)", collectParameter, []).option("--json", "output JSON").action(async (defIdRaw, options) => {
6752
7697
  validateOrgProjectPair(options);
6753
7698
  const defId = parsePositiveId(defIdRaw);
@@ -6783,20 +7728,42 @@ ${result.webUrl ?? "\u2014"}
6783
7728
  return command;
6784
7729
  }
6785
7730
  function createPipelineCommand() {
6786
- const command = new Command13("pipeline");
7731
+ const command = new Command14("pipeline");
6787
7732
  command.description("Manage Azure DevOps pipelines");
6788
7733
  command.addCommand(createPipelineListCommand());
6789
7734
  command.addCommand(createPipelineGetRunsCommand());
6790
7735
  command.addCommand(createPipelineWaitCommand());
6791
7736
  command.addCommand(createPipelineGetRunDetailCommand());
6792
7737
  command.addCommand(createPipelineLogsCommand());
7738
+ command.addCommand(createPipelineArtifactsCommand());
7739
+ command.addCommand(createPipelineArtifactDownloadCommand());
6793
7740
  command.addCommand(createPipelineTestsCommand());
6794
7741
  command.addCommand(createPipelineStartCommand());
6795
7742
  return command;
6796
7743
  }
6797
7744
 
6798
7745
  // src/commands/comments.ts
6799
- import { Command as Command14 } from "commander";
7746
+ import { Command as Command15 } from "commander";
7747
+ async function runCommentWrite(options, target, request, humanLine) {
7748
+ let context;
7749
+ try {
7750
+ context = resolveContext(options);
7751
+ const credential = await requireAuthCredential(context.org);
7752
+ const result = await request(context, credential);
7753
+ process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}
7754
+ ` : `${humanLine(result)}
7755
+ `);
7756
+ } catch (err) {
7757
+ if (target.commentId === void 0) {
7758
+ handleCommandError(err, target.id, context, "write");
7759
+ } else {
7760
+ handleCommentWriteError(err, target.id, target.commentId, context);
7761
+ }
7762
+ }
7763
+ }
7764
+ function commentFormat(options) {
7765
+ return options.markdown === true ? "markdown" : "html";
7766
+ }
6800
7767
  function writeError3(message) {
6801
7768
  process.stderr.write(`Error: ${message}
6802
7769
  `);
@@ -6816,7 +7783,7 @@ function formatComments(result, convertMarkdown) {
6816
7783
  return lines.join("\n");
6817
7784
  }
6818
7785
  function createCommentsListCommand() {
6819
- const command = new Command14("list");
7786
+ const command = new Command15("list");
6820
7787
  command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "convert HTML comment bodies to markdown").action(async (idStr, options) => {
6821
7788
  validateOrgProjectPair(options);
6822
7789
  const id = parseWorkItemId(idStr);
@@ -6844,47 +7811,101 @@ function createCommentsListCommand() {
6844
7811
  return command;
6845
7812
  }
6846
7813
  function createCommentsAddCommand() {
6847
- const command = new Command14("add");
7814
+ const command = new Command15("add");
6848
7815
  command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, text, options) => {
6849
7816
  validateOrgProjectPair(options);
6850
7817
  const id = parseWorkItemId(idStr);
6851
7818
  if (text.trim() === "") {
6852
7819
  writeError3("Comment text must be a non-empty string.");
6853
7820
  }
6854
- let context;
7821
+ await runCommentWrite(
7822
+ options,
7823
+ { id },
7824
+ (context, credential) => addWorkItemComment(context, id, credential, text, commentFormat(options)),
7825
+ (result) => `Added comment #${result.commentId} to work item #${result.workItemId}`
7826
+ );
7827
+ });
7828
+ return command;
7829
+ }
7830
+ function parseCommentId(value) {
7831
+ if (!/^\d+$/.test(value) || Number(value) < 1) {
7832
+ writeError3(`Invalid comment ID "${value}". Expected a positive integer.`);
7833
+ }
7834
+ return Number(value);
7835
+ }
7836
+ function resolveEditText(inline, file) {
7837
+ if (inline !== void 0 && file !== void 0) {
7838
+ writeError3("Cannot specify both inline text and --file.");
7839
+ }
7840
+ let body = inline;
7841
+ if (file !== void 0) {
6855
7842
  try {
6856
- context = resolveContext(options);
6857
- const credential = await requireAuthCredential(context.org);
6858
- const format = options.markdown === true ? "markdown" : "html";
6859
- const result = await addWorkItemComment(context, id, credential, text, format);
6860
- if (options.json) {
6861
- process.stdout.write(`${JSON.stringify(result, null, 2)}
6862
- `);
6863
- return;
6864
- }
6865
- process.stdout.write(`Added comment #${result.commentId} to work item #${result.workItemId}
6866
- `);
7843
+ body = readTextSource(file);
6867
7844
  } catch (err) {
6868
- handleCommandError(err, id, context, "write");
7845
+ writeError3(err.message);
6869
7846
  }
7847
+ }
7848
+ if (body === void 0 || body.trim() === "") {
7849
+ writeError3("Comment text must be a non-empty string. Pass the text inline or use --file <path>.");
7850
+ }
7851
+ return body;
7852
+ }
7853
+ function handleCommentWriteError(err, id, commentId, context) {
7854
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
7855
+ writeError3(
7856
+ `Comment ${commentId} not found on work item ${id} in ${context?.org}/${context?.project}.`
7857
+ );
7858
+ }
7859
+ handleCommandError(err, id, context, "write");
7860
+ }
7861
+ function createCommentsEditCommand() {
7862
+ const command = new Command15("edit");
7863
+ command.description("Edit an existing comment on a work item").argument("<id>", "work item ID").argument("<commentId>", "comment ID").argument("[text]", "new comment text (or use --file)").option("--file <path>", 'read the new comment text from a file ("-" for stdin)').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, commentIdStr, text, options) => {
7864
+ validateOrgProjectPair(options);
7865
+ const id = parseWorkItemId(idStr);
7866
+ const commentId = parseCommentId(commentIdStr);
7867
+ const body = resolveEditText(text, options.file);
7868
+ await runCommentWrite(
7869
+ options,
7870
+ { id, commentId },
7871
+ (context, credential) => updateWorkItemComment(context, id, commentId, credential, body, commentFormat(options)),
7872
+ (result) => `Updated comment #${result.commentId} on work item #${result.workItemId}`
7873
+ );
7874
+ });
7875
+ return command;
7876
+ }
7877
+ function createCommentsDeleteCommand() {
7878
+ const command = new Command15("delete");
7879
+ command.description("Delete a comment from a work item (no confirmation prompt)").argument("<id>", "work item ID").argument("<commentId>", "comment ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, commentIdStr, options) => {
7880
+ validateOrgProjectPair(options);
7881
+ const id = parseWorkItemId(idStr);
7882
+ const commentId = parseCommentId(commentIdStr);
7883
+ await runCommentWrite(
7884
+ options,
7885
+ { id, commentId },
7886
+ (context, credential) => deleteWorkItemComment(context, id, commentId, credential),
7887
+ (result) => `Deleted comment #${result.commentId} from work item #${result.workItemId}`
7888
+ );
6870
7889
  });
6871
7890
  return command;
6872
7891
  }
6873
7892
  function createCommentsCommand() {
6874
- const command = new Command14("comments");
7893
+ const command = new Command15("comments");
6875
7894
  command.description("Manage Azure DevOps work item comments");
6876
7895
  command.addCommand(createCommentsListCommand());
6877
7896
  command.addCommand(createCommentsAddCommand());
7897
+ command.addCommand(createCommentsEditCommand());
7898
+ command.addCommand(createCommentsDeleteCommand());
6878
7899
  return command;
6879
7900
  }
6880
7901
 
6881
7902
  // src/commands/download-attachment.ts
6882
- import { Command as Command15 } from "commander";
7903
+ import { Command as Command16 } from "commander";
6883
7904
  import { writeFile as writeFile2 } from "fs/promises";
6884
- import { existsSync as existsSync6 } from "fs";
7905
+ import { existsSync as existsSync7 } from "fs";
6885
7906
  import { join as join3 } from "path";
6886
7907
  function createDownloadAttachmentCommand() {
6887
- const command = new Command15("download-attachment");
7908
+ const command = new Command16("download-attachment");
6888
7909
  command.description("Download an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to download").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--output <dir>", "target directory for the downloaded file").action(
6889
7910
  async (idStr, filename, options) => {
6890
7911
  const id = parseWorkItemId(idStr);
@@ -6894,7 +7915,7 @@ function createDownloadAttachmentCommand() {
6894
7915
  context = resolveContext(options);
6895
7916
  const credential = await requireAuthCredential(context.org);
6896
7917
  const outputDir = options.output ?? ".";
6897
- if (!existsSync6(outputDir)) {
7918
+ if (!existsSync7(outputDir)) {
6898
7919
  process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
6899
7920
  `);
6900
7921
  process.exit(1);
@@ -6926,17 +7947,17 @@ function createDownloadAttachmentCommand() {
6926
7947
  }
6927
7948
 
6928
7949
  // src/commands/add-attachment.ts
6929
- import { Command as Command16 } from "commander";
6930
- import { existsSync as existsSync7, statSync } from "fs";
7950
+ import { Command as Command17 } from "commander";
7951
+ import { existsSync as existsSync8, statSync } from "fs";
6931
7952
  import { readFile } from "fs/promises";
6932
7953
  import { basename } from "path";
6933
7954
  function createAddAttachmentCommand() {
6934
- const command = new Command16("add-attachment");
7955
+ const command = new Command17("add-attachment");
6935
7956
  command.description("Attach a local file to an Azure DevOps work item").argument("<id>", "work item ID").argument("<file>", "path to the local file to upload").option("--comment <text>", "optional comment to store with the attachment").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
6936
7957
  async (idStr, file, options) => {
6937
7958
  const id = parseWorkItemId(idStr);
6938
7959
  validateOrgProjectPair(options);
6939
- if (!existsSync7(file)) {
7960
+ if (!existsSync8(file)) {
6940
7961
  process.stderr.write(`Error: File not found: ${file}
6941
7962
  `);
6942
7963
  process.exit(1);
@@ -6978,12 +7999,12 @@ function createAddAttachmentCommand() {
6978
7999
  }
6979
8000
 
6980
8001
  // src/commands/delete-attachment.ts
6981
- import { Command as Command17 } from "commander";
8002
+ import { Command as Command18 } from "commander";
6982
8003
  function formatUploadDate(iso) {
6983
8004
  return iso ? iso.slice(0, 10) : "unknown date";
6984
8005
  }
6985
8006
  function createDeleteAttachmentCommand() {
6986
- const command = new Command17("delete-attachment");
8007
+ const command = new Command18("delete-attachment");
6987
8008
  command.description("Remove an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to remove").option("--id <attachmentId>", "attachment GUID, to disambiguate when the filename is shared by more than one attachment").option("-y, --yes", "skip the interactive confirmation prompt").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
6988
8009
  async (idStr, filename, options) => {
6989
8010
  const id = parseWorkItemId(idStr);
@@ -7060,7 +8081,7 @@ function createDeleteAttachmentCommand() {
7060
8081
  }
7061
8082
 
7062
8083
  // src/commands/relations.ts
7063
- import { Command as Command18 } from "commander";
8084
+ import { Command as Command19 } from "commander";
7064
8085
 
7065
8086
  // src/services/relations-client.ts
7066
8087
  var API_VERSION2 = "7.1";
@@ -7277,7 +8298,7 @@ function parsePositiveInt(value, label) {
7277
8298
  return n;
7278
8299
  }
7279
8300
  function createRelationsCommand() {
7280
- const relations = new Command18("relations").description("Manage work item relations");
8301
+ const relations = new Command19("relations").description("Manage work item relations");
7281
8302
  addCommonOptions(
7282
8303
  relations.command("types").description("List all available work item relation types")
7283
8304
  ).action(async (opts) => {
@@ -7356,7 +8377,7 @@ function createRelationsCommand() {
7356
8377
 
7357
8378
  // src/services/update-check.ts
7358
8379
  import fs2 from "fs";
7359
- import path2 from "path";
8380
+ import path4 from "path";
7360
8381
  import os from "os";
7361
8382
  var THROTTLE_MS = 10 * 60 * 1e3;
7362
8383
  var FETCH_TIMEOUT_MS = 1500;
@@ -7374,7 +8395,7 @@ function skipsUpdateCheck(commandPath) {
7374
8395
  return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
7375
8396
  }
7376
8397
  function getCachePath() {
7377
- return path2.join(os.homedir(), ".azdo", "update-check.json");
8398
+ return path4.join(os.homedir(), ".azdo", "update-check.json");
7378
8399
  }
7379
8400
  function defaultReadCache() {
7380
8401
  try {
@@ -7386,7 +8407,7 @@ function defaultReadCache() {
7386
8407
  function defaultWriteCache(data) {
7387
8408
  try {
7388
8409
  const cachePath = getCachePath();
7389
- fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
8410
+ fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
7390
8411
  fs2.writeFileSync(cachePath, data);
7391
8412
  } catch {
7392
8413
  }
@@ -7472,11 +8493,12 @@ async function getUpdateNotice(opts) {
7472
8493
 
7473
8494
  // src/program.ts
7474
8495
  function createProgram() {
7475
- const program2 = new Command19();
8496
+ const program2 = new Command20();
7476
8497
  program2.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
7477
8498
  program2.option("--no-update-check", "Skip the check for a newer published version");
7478
8499
  program2.option("--trace <filepath>", "Append redacted HTTP request/response trace to a file (owner-read-only permissions)");
7479
8500
  program2.addCommand(createGetItemCommand());
8501
+ program2.addCommand(createListItemsCommand());
7480
8502
  program2.addCommand(createAuthCommand());
7481
8503
  program2.addCommand(createClearPatCommand());
7482
8504
  program2.addCommand(createConfigCommand());