azdo-cli 0.19.0-feature-037-api-error-surfacing.701 → 0.19.0-feature-038-pr-update.704

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 (2) hide show
  1. package/dist/index.js +202 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3750,6 +3750,28 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
3750
3750
  pullRequest: mapPullRequest(context, repo, data)
3751
3751
  };
3752
3752
  }
3753
+ async function updatePullRequest(context, repo, cred, prId, fields) {
3754
+ if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
3755
+ const overflow = fields.description.length - MAX_PR_DESCRIPTION_CHARS;
3756
+ throw new Error(
3757
+ `DESCRIPTION_TOO_LONG: description is ${fields.description.length} characters, exceeding the Azure DevOps limit of ${MAX_PR_DESCRIPTION_CHARS} characters. Shorten the description by at least ${overflow} characters.`
3758
+ );
3759
+ }
3760
+ const url = new URL(
3761
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}`
3762
+ );
3763
+ url.searchParams.set("api-version", "7.1");
3764
+ const response = await fetchWithErrors(url.toString(), {
3765
+ method: "PATCH",
3766
+ headers: {
3767
+ ...authHeaders(cred),
3768
+ "Content-Type": "application/json"
3769
+ },
3770
+ body: JSON.stringify(fields)
3771
+ });
3772
+ const data = await readJsonResponse(response);
3773
+ return mapPullRequest(context, repo, data);
3774
+ }
3753
3775
  async function getPullRequestThreads(context, repo, cred, prId) {
3754
3776
  const url = new URL(
3755
3777
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
@@ -4107,6 +4129,27 @@ function writeContractError(line) {
4107
4129
  process.exitCode = 1;
4108
4130
  }
4109
4131
  var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
4132
+ var STDIN_PATH = "-";
4133
+ function readTextSource(file) {
4134
+ if (file === STDIN_PATH) {
4135
+ try {
4136
+ return readFileSync5(0, "utf-8");
4137
+ } catch {
4138
+ writeError("Cannot read standard input.");
4139
+ return null;
4140
+ }
4141
+ }
4142
+ if (!existsSync5(file)) {
4143
+ writeError(`File not found: ${file}`);
4144
+ return null;
4145
+ }
4146
+ try {
4147
+ return readFileSync5(file, "utf-8");
4148
+ } catch {
4149
+ writeError(`Cannot read file: ${file}`);
4150
+ return null;
4151
+ }
4152
+ }
4110
4153
  function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4111
4154
  if (inline !== void 0 && file !== void 0) {
4112
4155
  writeError("Cannot specify both inline text and --file.");
@@ -4114,16 +4157,11 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4114
4157
  }
4115
4158
  let body;
4116
4159
  if (file !== void 0) {
4117
- if (!existsSync5(file)) {
4118
- writeError(`File not found: ${file}`);
4119
- return null;
4120
- }
4121
- try {
4122
- body = readFileSync5(file, "utf-8");
4123
- } catch {
4124
- writeError(`Cannot read file: ${file}`);
4160
+ const read = readTextSource(file);
4161
+ if (read === null) {
4125
4162
  return null;
4126
4163
  }
4164
+ body = read;
4127
4165
  } else if (inline !== void 0) {
4128
4166
  body = inline;
4129
4167
  } else {
@@ -4137,6 +4175,33 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4137
4175
  }
4138
4176
  return trimmed;
4139
4177
  }
4178
+ function resolveOptionalTextInput(inline, file, flag) {
4179
+ if (inline !== void 0 && file !== void 0) {
4180
+ writeError(`Cannot specify both --${flag} and --${flag}-file.`);
4181
+ return null;
4182
+ }
4183
+ if (inline === void 0 && file === void 0) {
4184
+ return void 0;
4185
+ }
4186
+ const label = flag === "title" ? "Title" : "Description";
4187
+ const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
4188
+ let value;
4189
+ if (file !== void 0) {
4190
+ const read = readTextSource(file);
4191
+ if (read === null) {
4192
+ return null;
4193
+ }
4194
+ value = read;
4195
+ } else {
4196
+ value = inline;
4197
+ }
4198
+ const trimmed = value.trim();
4199
+ if (trimmed === "") {
4200
+ writeError(emptyMessage);
4201
+ return null;
4202
+ }
4203
+ return trimmed;
4204
+ }
4140
4205
  function parseNonNegativeInt(raw) {
4141
4206
  if (!/^\d+$/.test(raw)) {
4142
4207
  return null;
@@ -4397,6 +4462,9 @@ function createPrOpenCommand() {
4397
4462
  withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
4398
4463
  "--description <description>",
4399
4464
  "pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
4465
+ ).option(
4466
+ "--description-file <path>",
4467
+ 'read the description from a UTF-8 file instead of --description; "-" reads standard input'
4400
4468
  ).option("--json", "output JSON").action(async (options) => {
4401
4469
  validateOrgProjectPair(options);
4402
4470
  const title = options.title?.trim();
@@ -4404,8 +4472,25 @@ function createPrOpenCommand() {
4404
4472
  writeError("--title is required for pull request creation.");
4405
4473
  return;
4406
4474
  }
4407
- const trimmedDescription = options.description?.trim();
4408
- const description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
4475
+ if (options.description !== void 0 && options.descriptionFile !== void 0) {
4476
+ writeError("Cannot specify both --description and --description-file.");
4477
+ return;
4478
+ }
4479
+ let description;
4480
+ if (options.descriptionFile !== void 0) {
4481
+ const fromFile = readTextSource(options.descriptionFile);
4482
+ if (fromFile === null) {
4483
+ return;
4484
+ }
4485
+ description = fromFile.trim();
4486
+ if (description === "") {
4487
+ writeError("Description must not be empty. Pass the text inline or use --description-file <path>.");
4488
+ return;
4489
+ }
4490
+ } else {
4491
+ const trimmedDescription = options.description?.trim();
4492
+ description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
4493
+ }
4409
4494
  let context;
4410
4495
  try {
4411
4496
  const resolved = await resolvePrCommandContext(options);
@@ -4445,6 +4530,112 @@ ${result.pullRequest.url ?? "\u2014"}
4445
4530
  });
4446
4531
  return command;
4447
4532
  }
4533
+ function handlePrUpdateError(err, context) {
4534
+ const message = err instanceof Error ? err.message : "";
4535
+ if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
4536
+ writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
4537
+ return;
4538
+ }
4539
+ handlePrCommandError(err, context, "write");
4540
+ }
4541
+ function joinFieldNames(fields) {
4542
+ return fields.join(" and ");
4543
+ }
4544
+ async function runPrUpdate(options) {
4545
+ if (options.titleFile === STDIN_PATH && options.descriptionFile === STDIN_PATH) {
4546
+ writeError('Cannot read standard input twice; only one of --title-file and --description-file may be "-".');
4547
+ return;
4548
+ }
4549
+ const title = resolveOptionalTextInput(options.title, options.titleFile, "title");
4550
+ if (title === null) {
4551
+ return;
4552
+ }
4553
+ const description = resolveOptionalTextInput(options.description, options.descriptionFile, "description");
4554
+ if (description === null) {
4555
+ return;
4556
+ }
4557
+ if (title === void 0 && description === void 0) {
4558
+ writeError("pr update requires at least one of --title, --title-file, --description or --description-file.");
4559
+ return;
4560
+ }
4561
+ let context;
4562
+ try {
4563
+ const target = await resolvePullRequestTarget(options);
4564
+ if (target === null) {
4565
+ return;
4566
+ }
4567
+ context = target.context;
4568
+ const current = target.pullRequest;
4569
+ const fields = {};
4570
+ const updatedFields = [];
4571
+ const requestedFields = [];
4572
+ if (title !== void 0) {
4573
+ requestedFields.push("title");
4574
+ if (title !== current.title) {
4575
+ fields.title = title;
4576
+ updatedFields.push("title");
4577
+ }
4578
+ }
4579
+ if (description !== void 0) {
4580
+ requestedFields.push("description");
4581
+ if (description !== (current.description ?? "")) {
4582
+ fields.description = description;
4583
+ updatedFields.push("description");
4584
+ }
4585
+ }
4586
+ if (updatedFields.length === 0) {
4587
+ const noopResult = {
4588
+ pullRequestId: current.id,
4589
+ title: current.title,
4590
+ description: current.description ?? null,
4591
+ url: current.url,
4592
+ noop: true,
4593
+ updatedFields: []
4594
+ };
4595
+ if (options.json) {
4596
+ process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
4597
+ `);
4598
+ return;
4599
+ }
4600
+ process.stdout.write(
4601
+ `Pull request #${current.id} already has the requested ${joinFieldNames(requestedFields)}; nothing to update.
4602
+ `
4603
+ );
4604
+ return;
4605
+ }
4606
+ const updated = await updatePullRequest(target.context, target.repo, target.pat, current.id, fields);
4607
+ const result = {
4608
+ pullRequestId: updated.id,
4609
+ title: updated.title,
4610
+ description: updated.description ?? null,
4611
+ url: updated.url,
4612
+ noop: false,
4613
+ updatedFields
4614
+ };
4615
+ if (options.json) {
4616
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4617
+ `);
4618
+ return;
4619
+ }
4620
+ process.stdout.write(
4621
+ `Updated pull request #${updated.id} (${updatedFields.join(", ")}).
4622
+ ${updated.url ?? "\u2014"}
4623
+ `
4624
+ );
4625
+ } catch (err) {
4626
+ handlePrUpdateError(err, context);
4627
+ }
4628
+ }
4629
+ function createPrUpdateCommand() {
4630
+ const command = new Command12("update");
4631
+ 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(
4632
+ "--description <description>",
4633
+ 'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
4634
+ ).option("--description-file <path>", 'read the new description from a UTF-8 file; "-" reads standard input').option("--json", "output JSON").action(async (_options, command2) => {
4635
+ await runPrUpdate(mergedPrOptions(command2));
4636
+ });
4637
+ return command;
4638
+ }
4448
4639
  function createPrCommentsCommand() {
4449
4640
  const command = new Command12("comments");
4450
4641
  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) => {
@@ -5224,6 +5415,7 @@ function createPrCommand() {
5224
5415
  command.addCommand(createPrListCommand());
5225
5416
  command.addCommand(createPrStatusCommand());
5226
5417
  command.addCommand(createPrOpenCommand());
5418
+ command.addCommand(createPrUpdateCommand());
5227
5419
  command.addCommand(createPrCommentsCommand());
5228
5420
  command.addCommand(createPrCommentResolveCommand());
5229
5421
  command.addCommand(createPrCommentReopenCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.19.0-feature-037-api-error-surfacing.701",
3
+ "version": "0.19.0-feature-038-pr-update.704",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {