azdo-cli 0.22.0-develop.822 → 0.22.0-develop.828

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +320 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -105,6 +105,7 @@ git log -1 --format=%B | azdo pr comments add --file - --pr-number 64 # "-"
105
105
  azdo pr open --title "Fix the thing" --description "Because X was broken"
106
106
  azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
107
107
  azdo pr open --title "Fix the thing" --description-file body.md # or --description-file - to pipe it in
108
+ azdo pr open --title "Fix the thing" --target master --source feature/x --draft --work-item 1234 --label bug # --work-item/--label repeat; --json adds top-level id and url
108
109
  # The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
109
110
  # the command says by how much and creates nothing. See docs/commands.md.
110
111
 
package/dist/index.js CHANGED
@@ -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;
@@ -279,11 +280,29 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
279
280
  process.exitCode = 1;
280
281
  }
281
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
+ }
282
301
 
283
302
  // src/services/image-download.ts
284
303
  import { Jimp } from "jimp";
285
304
  import { writeFile } from "fs/promises";
286
- import { existsSync } from "fs";
305
+ import { existsSync as existsSync2 } from "fs";
287
306
  import { tmpdir } from "os";
288
307
  import { join } from "path";
289
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})/;
@@ -366,7 +385,7 @@ function resolveImageDownloadOptions(flags) {
366
385
  maxWidth = parsed;
367
386
  }
368
387
  const outputDir = flags.imagesPath ?? tmpdir();
369
- if (flags.imagesPath !== void 0 && !existsSync(outputDir)) {
388
+ if (flags.imagesPath !== void 0 && !existsSync2(outputDir)) {
370
389
  throw new Error(`Images path "${outputDir}" does not exist.`);
371
390
  }
372
391
  return { enabled, maxWidth, outputDir };
@@ -668,6 +687,11 @@ function buildWorkItemCommentsUrl(context, id) {
668
687
  url.searchParams.set("api-version", "7.1-preview.4");
669
688
  return url;
670
689
  }
690
+ function buildWorkItemCommentUrl(context, id, commentId) {
691
+ const url = buildWorkItemCommentsUrl(context, id);
692
+ url.pathname = `${url.pathname}/${commentId}`;
693
+ return url;
694
+ }
671
695
  function mapWorkItemComment(comment, fallbackWorkItemId) {
672
696
  return {
673
697
  id: comment.id ?? comment.commentId ?? 0,
@@ -892,16 +916,11 @@ async function listWorkItemComments(context, id, cred) {
892
916
  comments
893
917
  };
894
918
  }
895
- async function addWorkItemComment(context, id, cred, text, format = "html") {
896
- const url = buildWorkItemCommentsUrl(context, id);
897
- url.searchParams.set("format", format);
919
+ async function sendCommentRequest(url, cred, method, body) {
898
920
  const response = await fetchWithErrors(url.toString(), {
899
- method: "POST",
900
- headers: {
901
- ...authHeaders(cred),
902
- "Content-Type": "application/json"
903
- },
904
- body: JSON.stringify({ text })
921
+ method,
922
+ headers: body === void 0 ? authHeaders(cred) : { ...authHeaders(cred), "Content-Type": "application/json" },
923
+ body: body === void 0 ? void 0 : JSON.stringify(body)
905
924
  });
906
925
  if (response.status === 400) {
907
926
  const serverMessage = await readResponseMessage(response) ?? "Unknown error";
@@ -910,16 +929,50 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
910
929
  if (!response.ok) {
911
930
  throw httpError(response);
912
931
  }
932
+ return response;
933
+ }
934
+ async function writeCommentText(url, cred, method, text, format, fallback) {
935
+ url.searchParams.set("format", format);
936
+ const response = await sendCommentRequest(url, cred, method, { text });
913
937
  const data = await response.json();
914
938
  return {
915
- workItemId: data.workItemId ?? id,
916
- commentId: data.commentId ?? data.id ?? 0,
939
+ workItemId: data.workItemId ?? fallback.workItemId,
940
+ commentId: data.commentId ?? data.id ?? fallback.commentId,
917
941
  text: typeof data.text === "string" ? data.text : text,
918
942
  author: data.createdBy?.displayName ?? null,
919
943
  createdAt: data.createdDate ?? null,
944
+ modifiedAt: data.modifiedDate ?? null,
920
945
  url: data.url ?? null
921
946
  };
922
947
  }
948
+ async function addWorkItemComment(context, id, cred, text, format = "html") {
949
+ const written = await writeCommentText(
950
+ buildWorkItemCommentsUrl(context, id),
951
+ cred,
952
+ "POST",
953
+ text,
954
+ format,
955
+ { workItemId: id, commentId: 0 }
956
+ );
957
+ return {
958
+ workItemId: written.workItemId,
959
+ commentId: written.commentId,
960
+ text: written.text,
961
+ author: written.author,
962
+ createdAt: written.createdAt,
963
+ url: written.url
964
+ };
965
+ }
966
+ async function updateWorkItemComment(context, id, commentId, cred, text, format = "html") {
967
+ return writeCommentText(buildWorkItemCommentUrl(context, id, commentId), cred, "PATCH", text, format, {
968
+ workItemId: id,
969
+ commentId
970
+ });
971
+ }
972
+ async function deleteWorkItemComment(context, id, commentId, cred) {
973
+ await sendCommentRequest(buildWorkItemCommentUrl(context, id, commentId), cred, "DELETE");
974
+ return { workItemId: id, commentId, deleted: true };
975
+ }
923
976
  async function updateWorkItem(context, id, cred, fieldName, operations) {
924
977
  const result = await applyWorkItemPatch(context, id, cred, operations);
925
978
  const title = result.fields["System.Title"];
@@ -1093,7 +1146,7 @@ async function queryWorkItems(context, cred, filter) {
1093
1146
 
1094
1147
  // src/services/auth.ts
1095
1148
  import { createInterface } from "readline";
1096
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1149
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1097
1150
  import { dirname as dirname2, join as join2 } from "path";
1098
1151
 
1099
1152
  // src/services/oauth-device-code.ts
@@ -1263,8 +1316,8 @@ async function promptForPat() {
1263
1316
  });
1264
1317
  }
1265
1318
  function patFromEnvFile(envFile) {
1266
- if (!existsSync2(envFile)) return null;
1267
- const contents = readFileSync2(envFile, "utf8");
1319
+ if (!existsSync3(envFile)) return null;
1320
+ const contents = readFileSync3(envFile, "utf8");
1268
1321
  for (const line of contents.split("\n")) {
1269
1322
  const match = line.match(/^AZDO_PAT\s*=([^\n\r]+)$/);
1270
1323
  if (match === null) continue;
@@ -3318,7 +3371,7 @@ function createGetMdFieldCommand() {
3318
3371
  }
3319
3372
 
3320
3373
  // src/commands/set-md-field.ts
3321
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
3374
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
3322
3375
  import { Command as Command10 } from "commander";
3323
3376
  function fail(message) {
3324
3377
  process.stderr.write(`Error: ${message}
@@ -3338,11 +3391,11 @@ function resolveContent(inlineContent, options) {
3338
3391
  return null;
3339
3392
  }
3340
3393
  function readFileContent(filePath) {
3341
- if (!existsSync3(filePath)) {
3394
+ if (!existsSync4(filePath)) {
3342
3395
  fail(`File not found: ${filePath}`);
3343
3396
  }
3344
3397
  try {
3345
- return readFileSync3(filePath, "utf-8");
3398
+ return readFileSync4(filePath, "utf-8");
3346
3399
  } catch {
3347
3400
  fail(`Cannot read file: ${filePath}`);
3348
3401
  }
@@ -3407,7 +3460,7 @@ function createSetMdFieldCommand() {
3407
3460
  }
3408
3461
 
3409
3462
  // src/commands/upsert.ts
3410
- import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
3463
+ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync } from "fs";
3411
3464
  import { Command as Command11 } from "commander";
3412
3465
 
3413
3466
  // src/services/task-document.ts
@@ -3571,12 +3624,12 @@ function loadSourceContent(options) {
3571
3624
  return { content: options.content };
3572
3625
  }
3573
3626
  const filePath = options.file;
3574
- if (!existsSync4(filePath)) {
3627
+ if (!existsSync5(filePath)) {
3575
3628
  fail2(`File not found: ${filePath}`);
3576
3629
  }
3577
3630
  try {
3578
3631
  return {
3579
- content: readFileSync4(filePath, "utf-8"),
3632
+ content: readFileSync5(filePath, "utf-8"),
3580
3633
  sourceFile: filePath
3581
3634
  };
3582
3635
  } catch {
@@ -3795,7 +3848,6 @@ function createListFieldsCommand() {
3795
3848
  }
3796
3849
 
3797
3850
  // src/commands/pr.ts
3798
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3799
3851
  import { Command as Command13 } from "commander";
3800
3852
 
3801
3853
  // src/services/pr-client.ts
@@ -4130,43 +4182,26 @@ function formatContributions(composed) {
4130
4182
  function describeDescriptionBudget(composed) {
4131
4183
  return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
4132
4184
  }
4133
- async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
4134
- const existing = await listPullRequests(context, repo, cred, sourceBranch, {
4135
- status: "active",
4136
- targetBranch: "develop"
4137
- });
4138
- if (existing.length === 1) {
4139
- return {
4140
- branch: sourceBranch,
4141
- targetBranch: "develop",
4142
- created: false,
4143
- pullRequest: existing[0]
4144
- };
4145
- }
4146
- if (existing.length > 1) {
4147
- throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
4148
- }
4149
- const repository = await getRepository(context, repo, cred);
4150
- const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
4151
- const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
4152
- const composed = composeDescription(description, template);
4153
- if (composed === null) {
4154
- throw new Error("DESCRIPTION_REQUIRED");
4155
- }
4156
- if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
4157
- throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
4158
- }
4185
+ function buildOpenPayload(sourceBranch, targetBranch, title, description, options) {
4159
4186
  const payload = {
4160
4187
  sourceRefName: `refs/heads/${sourceBranch}`,
4161
- targetRefName: "refs/heads/develop",
4188
+ targetRefName: `refs/heads/${targetBranch}`,
4162
4189
  title,
4163
- description: composed.text
4190
+ description
4164
4191
  };
4192
+ if (options.isDraft) {
4193
+ payload.isDraft = true;
4194
+ }
4195
+ if (options.labels && options.labels.length > 0) {
4196
+ payload.labels = options.labels.map((name) => ({ name }));
4197
+ }
4198
+ return payload;
4199
+ }
4200
+ async function createPullRequest(context, repo, cred, payload, composed) {
4165
4201
  const url = new URL(
4166
4202
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
4167
4203
  );
4168
4204
  url.searchParams.set("api-version", "7.1");
4169
- let data;
4170
4205
  try {
4171
4206
  const response = await fetchWithErrors(url.toString(), {
4172
4207
  method: "POST",
@@ -4176,19 +4211,69 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
4176
4211
  },
4177
4212
  body: JSON.stringify(payload)
4178
4213
  });
4179
- data = await readJsonResponse(response);
4214
+ return await readJsonResponse(response);
4180
4215
  } catch (err) {
4181
4216
  if (err instanceof Error && err.message.startsWith("HTTP_400")) {
4182
4217
  throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
4183
4218
  }
4184
4219
  throw err;
4185
4220
  }
4186
- return {
4221
+ }
4222
+ function linkWorkItems(context, repo, cred, prId, workItemIds) {
4223
+ return Promise.all(
4224
+ workItemIds.map(
4225
+ (id) => linkWorkItemToPullRequest(context, repo, cred, prId, id).then(
4226
+ () => ({ id, linked: true }),
4227
+ (err) => ({ id, linked: false, error: err instanceof Error ? err.message : String(err) })
4228
+ )
4229
+ )
4230
+ );
4231
+ }
4232
+ async function openPullRequest(context, repo, cred, sourceBranch, title, description, options = {}) {
4233
+ const targetBranch = options.targetBranch ?? "develop";
4234
+ const existing = await listPullRequests(context, repo, cred, sourceBranch, {
4235
+ status: "active",
4236
+ targetBranch
4237
+ });
4238
+ if (existing.length === 1) {
4239
+ return {
4240
+ branch: sourceBranch,
4241
+ targetBranch,
4242
+ created: false,
4243
+ pullRequest: existing[0]
4244
+ };
4245
+ }
4246
+ if (existing.length > 1) {
4247
+ throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest2) => pullRequest2.id).join(",")}`);
4248
+ }
4249
+ const repository = await getRepository(context, repo, cred);
4250
+ const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
4251
+ const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch);
4252
+ const composed = composeDescription(description, template);
4253
+ if (composed === null) {
4254
+ throw new Error("DESCRIPTION_REQUIRED");
4255
+ }
4256
+ if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
4257
+ throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
4258
+ }
4259
+ const data = await createPullRequest(
4260
+ context,
4261
+ repo,
4262
+ cred,
4263
+ buildOpenPayload(sourceBranch, targetBranch, title, composed.text, options),
4264
+ composed
4265
+ );
4266
+ const pullRequest = mapPullRequest(context, repo, data);
4267
+ const result = {
4187
4268
  branch: sourceBranch,
4188
- targetBranch: "develop",
4269
+ targetBranch,
4189
4270
  created: true,
4190
- pullRequest: mapPullRequest(context, repo, data)
4271
+ pullRequest
4191
4272
  };
4273
+ if (options.workItemIds && options.workItemIds.length > 0) {
4274
+ result.workItems = await linkWorkItems(context, repo, cred, pullRequest.id, options.workItemIds);
4275
+ }
4276
+ return result;
4192
4277
  }
4193
4278
  async function updatePullRequest(context, repo, cred, prId, fields) {
4194
4279
  if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
@@ -4615,24 +4700,11 @@ function writeContractError(line) {
4615
4700
  process.exitCode = 1;
4616
4701
  }
4617
4702
  var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
4618
- var STDIN_PATH = "-";
4619
- function readTextSource(file) {
4620
- if (file === STDIN_PATH) {
4621
- try {
4622
- return readFileSync5(0, "utf-8");
4623
- } catch {
4624
- writeError("Cannot read standard input.");
4625
- return null;
4626
- }
4627
- }
4628
- if (!existsSync5(file)) {
4629
- writeError(`File not found: ${file}`);
4630
- return null;
4631
- }
4703
+ function readTextSource2(file) {
4632
4704
  try {
4633
- return readFileSync5(file, "utf-8");
4634
- } catch {
4635
- writeError(`Cannot read file: ${file}`);
4705
+ return readTextSource(file);
4706
+ } catch (err) {
4707
+ writeError(err.message);
4636
4708
  return null;
4637
4709
  }
4638
4710
  }
@@ -4643,7 +4715,7 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4643
4715
  }
4644
4716
  let body;
4645
4717
  if (file !== void 0) {
4646
- const read = readTextSource(file);
4718
+ const read = readTextSource2(file);
4647
4719
  if (read === null) {
4648
4720
  return null;
4649
4721
  }
@@ -4673,7 +4745,7 @@ function resolveOptionalTextInput(inline, file, flag) {
4673
4745
  const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
4674
4746
  let value;
4675
4747
  if (file !== void 0) {
4676
- const read = readTextSource(file);
4748
+ const read = readTextSource2(file);
4677
4749
  if (read === null) {
4678
4750
  return null;
4679
4751
  }
@@ -4970,11 +5042,11 @@ function createPrStatusCommand() {
4970
5042
  });
4971
5043
  return command;
4972
5044
  }
4973
- function handlePrOpenError(err, context) {
5045
+ function handlePrOpenError(err, context, targetBranch = "develop") {
4974
5046
  const message = err instanceof Error ? err.message : "";
4975
5047
  if (message.startsWith("AMBIGUOUS_PRS:")) {
4976
5048
  const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
4977
- writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
5049
+ writeError(`Multiple active pull requests already exist for this branch targeting ${targetBranch}: ${ids}. Use pr status to review them.`);
4978
5050
  return;
4979
5051
  }
4980
5052
  if (message === "DESCRIPTION_REQUIRED") {
@@ -4987,13 +5059,54 @@ function handlePrOpenError(err, context) {
4987
5059
  }
4988
5060
  handlePrCommandError(err, context, "write");
4989
5061
  }
5062
+ function collectOption(value, previous) {
5063
+ return [...previous, value];
5064
+ }
5065
+ function stripHeadsPrefix(branch) {
5066
+ return branch.trim().replace(/^refs\/heads\//, "");
5067
+ }
5068
+ function resolveOpenOptions(options) {
5069
+ const targetBranch = options.target === void 0 ? "develop" : stripHeadsPrefix(options.target);
5070
+ const source = options.source === void 0 ? void 0 : stripHeadsPrefix(options.source);
5071
+ if (!targetBranch) {
5072
+ writeError("--target must not be empty.");
5073
+ return null;
5074
+ }
5075
+ if (source !== void 0 && !source) {
5076
+ writeError("--source must not be empty.");
5077
+ return null;
5078
+ }
5079
+ const workItemIds = [];
5080
+ for (const raw of options.workItem ?? []) {
5081
+ const id = Number(raw.trim());
5082
+ if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(id) || id <= 0) {
5083
+ writeError(`--work-item must be a positive integer, got "${raw}".`);
5084
+ return null;
5085
+ }
5086
+ if (!workItemIds.includes(id)) {
5087
+ workItemIds.push(id);
5088
+ }
5089
+ }
5090
+ const labels = [];
5091
+ for (const raw of options.label ?? []) {
5092
+ const name = raw.trim();
5093
+ if (!name) {
5094
+ writeError("--label must not be empty.");
5095
+ return null;
5096
+ }
5097
+ if (!labels.includes(name)) {
5098
+ labels.push(name);
5099
+ }
5100
+ }
5101
+ return { targetBranch, source, isDraft: options.draft === true, labels, workItemIds };
5102
+ }
4990
5103
  function resolveOpenDescription(inline, file) {
4991
5104
  if (inline !== void 0 && file !== void 0) {
4992
5105
  writeError("Cannot specify both --description and --description-file.");
4993
5106
  return null;
4994
5107
  }
4995
5108
  if (file !== void 0) {
4996
- const fromFile = readTextSource(file);
5109
+ const fromFile = readTextSource2(file);
4997
5110
  if (fromFile === null) {
4998
5111
  return null;
4999
5112
  }
@@ -5012,13 +5125,18 @@ function resolveOpenDescription(inline, file) {
5012
5125
  }
5013
5126
  function createPrOpenCommand() {
5014
5127
  const command = new Command13("open");
5015
- withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
5128
+ withCommonPrOptions(command).description("Open a pull request (default: from the current branch to develop)").option("--title <title>", "pull request title").option(
5016
5129
  "--description <description>",
5017
5130
  "pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
5018
5131
  ).option(
5019
5132
  "--description-file <path>",
5020
5133
  'read the description from a UTF-8 file instead of --description; "-" reads standard input'
5021
- ).option("--json", "output JSON").action(async (options) => {
5134
+ ).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(
5135
+ "--work-item <id>",
5136
+ "link a work item to the pull request (positive integer; repeatable)",
5137
+ collectOption,
5138
+ []
5139
+ ).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) => {
5022
5140
  validateOrgProjectPair(options);
5023
5141
  const title = options.title?.trim();
5024
5142
  if (!title) {
@@ -5029,45 +5147,71 @@ function createPrOpenCommand() {
5029
5147
  if (description === null) {
5030
5148
  return;
5031
5149
  }
5150
+ const openOptions = resolveOpenOptions(options);
5151
+ if (openOptions === null) {
5152
+ return;
5153
+ }
5032
5154
  let context;
5033
5155
  try {
5034
- const resolved = await resolvePrCommandContext(options);
5035
- context = resolved.context;
5036
- if (resolved.branch === "develop") {
5037
- writeError("Pull request creation requires a source branch other than develop.");
5156
+ const openBranch = openOptions.source ?? getCurrentBranch();
5157
+ const target = openOptions.targetBranch;
5158
+ if (openBranch === target) {
5159
+ writeError(
5160
+ 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}).`
5161
+ );
5038
5162
  return;
5039
5163
  }
5040
- const openBranch = resolved.branch;
5164
+ const resolved = await resolvePrCommandContext(options, { requireBranch: false });
5165
+ context = resolved.context;
5041
5166
  const result = await openPullRequest(
5042
5167
  resolved.context,
5043
5168
  resolved.repo,
5044
5169
  resolved.pat,
5045
5170
  openBranch,
5046
5171
  title,
5047
- description
5172
+ description,
5173
+ openOptions
5048
5174
  );
5049
5175
  if (options.json) {
5050
- process.stdout.write(`${JSON.stringify(result, null, 2)}
5176
+ const output = { ...result, id: result.pullRequest.id, url: result.pullRequest.url };
5177
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
5051
5178
  `);
5179
+ reportWorkItemLinkFailures(result);
5052
5180
  return;
5053
5181
  }
5054
5182
  if (result.created) {
5055
5183
  process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
5056
5184
  ${result.pullRequest.url ?? "\u2014"}
5057
5185
  `);
5186
+ reportWorkItemLinkFailures(result);
5058
5187
  return;
5059
5188
  }
5060
5189
  process.stdout.write(
5061
- `Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
5190
+ `Active pull request already exists for ${openBranch} -> ${result.targetBranch}: #${result.pullRequest.id}
5062
5191
  ${result.pullRequest.url ?? "\u2014"}
5063
5192
  `
5064
5193
  );
5194
+ if (openOptions.isDraft || openOptions.labels?.length || openOptions.workItemIds?.length) {
5195
+ const hint = openOptions.workItemIds?.length ? " Use pr work-items link to link a work item." : "";
5196
+ process.stdout.write(
5197
+ `--draft, --label and --work-item were not applied; the existing pull request was left unchanged.${hint}
5198
+ `
5199
+ );
5200
+ }
5065
5201
  } catch (err) {
5066
- handlePrOpenError(err, context);
5202
+ handlePrOpenError(err, context, openOptions.targetBranch);
5067
5203
  }
5068
5204
  });
5069
5205
  return command;
5070
5206
  }
5207
+ function reportWorkItemLinkFailures(result) {
5208
+ const failed = (result.workItems ?? []).filter((item) => !item.linked);
5209
+ if (failed.length === 0) return;
5210
+ const detail = failed.map((item) => `#${item.id} (${item.error ?? "unknown error"})`).join(", ");
5211
+ writeError(
5212
+ `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}`
5213
+ );
5214
+ }
5071
5215
  function handlePrUpdateError(err, context) {
5072
5216
  const message = err instanceof Error ? err.message : "";
5073
5217
  if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
@@ -7534,6 +7678,26 @@ function createPipelineCommand() {
7534
7678
 
7535
7679
  // src/commands/comments.ts
7536
7680
  import { Command as Command15 } from "commander";
7681
+ async function runCommentWrite(options, target, request, humanLine) {
7682
+ let context;
7683
+ try {
7684
+ context = resolveContext(options);
7685
+ const credential = await requireAuthCredential(context.org);
7686
+ const result = await request(context, credential);
7687
+ process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}
7688
+ ` : `${humanLine(result)}
7689
+ `);
7690
+ } catch (err) {
7691
+ if (target.commentId === void 0) {
7692
+ handleCommandError(err, target.id, context, "write");
7693
+ } else {
7694
+ handleCommentWriteError(err, target.id, target.commentId, context);
7695
+ }
7696
+ }
7697
+ }
7698
+ function commentFormat(options) {
7699
+ return options.markdown === true ? "markdown" : "html";
7700
+ }
7537
7701
  function writeError3(message) {
7538
7702
  process.stderr.write(`Error: ${message}
7539
7703
  `);
@@ -7588,22 +7752,74 @@ function createCommentsAddCommand() {
7588
7752
  if (text.trim() === "") {
7589
7753
  writeError3("Comment text must be a non-empty string.");
7590
7754
  }
7591
- let context;
7755
+ await runCommentWrite(
7756
+ options,
7757
+ { id },
7758
+ (context, credential) => addWorkItemComment(context, id, credential, text, commentFormat(options)),
7759
+ (result) => `Added comment #${result.commentId} to work item #${result.workItemId}`
7760
+ );
7761
+ });
7762
+ return command;
7763
+ }
7764
+ function parseCommentId(value) {
7765
+ if (!/^\d+$/.test(value) || Number(value) < 1) {
7766
+ writeError3(`Invalid comment ID "${value}". Expected a positive integer.`);
7767
+ }
7768
+ return Number(value);
7769
+ }
7770
+ function resolveEditText(inline, file) {
7771
+ if (inline !== void 0 && file !== void 0) {
7772
+ writeError3("Cannot specify both inline text and --file.");
7773
+ }
7774
+ let body = inline;
7775
+ if (file !== void 0) {
7592
7776
  try {
7593
- context = resolveContext(options);
7594
- const credential = await requireAuthCredential(context.org);
7595
- const format = options.markdown === true ? "markdown" : "html";
7596
- const result = await addWorkItemComment(context, id, credential, text, format);
7597
- if (options.json) {
7598
- process.stdout.write(`${JSON.stringify(result, null, 2)}
7599
- `);
7600
- return;
7601
- }
7602
- process.stdout.write(`Added comment #${result.commentId} to work item #${result.workItemId}
7603
- `);
7777
+ body = readTextSource(file);
7604
7778
  } catch (err) {
7605
- handleCommandError(err, id, context, "write");
7779
+ writeError3(err.message);
7606
7780
  }
7781
+ }
7782
+ if (body === void 0 || body.trim() === "") {
7783
+ writeError3("Comment text must be a non-empty string. Pass the text inline or use --file <path>.");
7784
+ }
7785
+ return body;
7786
+ }
7787
+ function handleCommentWriteError(err, id, commentId, context) {
7788
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
7789
+ writeError3(
7790
+ `Comment ${commentId} not found on work item ${id} in ${context?.org}/${context?.project}.`
7791
+ );
7792
+ }
7793
+ handleCommandError(err, id, context, "write");
7794
+ }
7795
+ function createCommentsEditCommand() {
7796
+ const command = new Command15("edit");
7797
+ 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) => {
7798
+ validateOrgProjectPair(options);
7799
+ const id = parseWorkItemId(idStr);
7800
+ const commentId = parseCommentId(commentIdStr);
7801
+ const body = resolveEditText(text, options.file);
7802
+ await runCommentWrite(
7803
+ options,
7804
+ { id, commentId },
7805
+ (context, credential) => updateWorkItemComment(context, id, commentId, credential, body, commentFormat(options)),
7806
+ (result) => `Updated comment #${result.commentId} on work item #${result.workItemId}`
7807
+ );
7808
+ });
7809
+ return command;
7810
+ }
7811
+ function createCommentsDeleteCommand() {
7812
+ const command = new Command15("delete");
7813
+ 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) => {
7814
+ validateOrgProjectPair(options);
7815
+ const id = parseWorkItemId(idStr);
7816
+ const commentId = parseCommentId(commentIdStr);
7817
+ await runCommentWrite(
7818
+ options,
7819
+ { id, commentId },
7820
+ (context, credential) => deleteWorkItemComment(context, id, commentId, credential),
7821
+ (result) => `Deleted comment #${result.commentId} from work item #${result.workItemId}`
7822
+ );
7607
7823
  });
7608
7824
  return command;
7609
7825
  }
@@ -7612,6 +7828,8 @@ function createCommentsCommand() {
7612
7828
  command.description("Manage Azure DevOps work item comments");
7613
7829
  command.addCommand(createCommentsListCommand());
7614
7830
  command.addCommand(createCommentsAddCommand());
7831
+ command.addCommand(createCommentsEditCommand());
7832
+ command.addCommand(createCommentsDeleteCommand());
7615
7833
  return command;
7616
7834
  }
7617
7835
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.22.0-develop.822",
3
+ "version": "0.22.0-develop.828",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {