azdo-cli 0.19.0-develop.703 → 0.19.0-develop.717

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 +17 -1
  2. package/dist/index.js +427 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,8 @@ Azure DevOps CLI focused on work item read/write workflows.
15
15
  - Attach a local file to a work item, or remove a named attachment (`add-attachment`, `delete-attachment`)
16
16
  - Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
17
17
  - Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
18
- - Check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
18
+ - Check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), update an existing PR's title or description (`pr update`), abandon or reactivate one (`pr abandon` / `pr reactivate`), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
19
+ - Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
19
20
  - Persist org/project/default fields in local config (`config`)
20
21
  - List all fields of a work item (`list-fields`)
21
22
  - Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. See [docs/authentication.md](docs/authentication.md).
@@ -85,13 +86,28 @@ azdo pr comments edit 148 --file plan.md --pr-number 64 # rewrite it in p
85
86
  azdo pr comments reply 148 "Great suggestion, I'll address it." # human-readable output
86
87
  azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
87
88
  azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
89
+ git log -1 --format=%B | azdo pr comments add --file - --pr-number 64 # "-" reads standard input
88
90
 
89
91
  # Open a pull request — description from a repo template when you don't pass one
90
92
  azdo pr open --title "Fix the thing" --description "Because X was broken"
91
93
  azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
94
+ azdo pr open --title "Fix the thing" --description-file body.md # or --description-file - to pipe it in
92
95
  # The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
93
96
  # the command says by how much and creates nothing. See docs/commands.md.
94
97
 
98
+ # Fix a title or description after the fact — a PR is no longer write-once
99
+ azdo pr update --pr-number 64 --title "A real title" # description untouched
100
+ azdo pr update --pr-number 64 --description-file body.md # replaces literally; no template is prepended
101
+ cat body.md | azdo pr edit --pr-number 64 --description-file - # "edit" is an alias for "update"
102
+ azdo pr update --pr-number 64 --title "A real title" --json # { pullRequestId, title, description, url, noop, updatedFields }
103
+ # Re-running with the values it already has writes nothing and reports "noop": true.
104
+
105
+ # Abandon a PR you opened by mistake — reversible, never prompts, nothing is deleted
106
+ azdo pr abandon --pr-number 64 # "close" is an alias; it does NOT complete/merge the PR
107
+ azdo pr reactivate --pr-number 64 # bring it back; --pr-number-less lookup searches abandoned PRs
108
+ azdo pr abandon --pr-number 64 --json # { pullRequestId, title, status, previousStatus, url, noop }
109
+ # Already abandoned? noop, exit 0, no write. Already completed? exit 1, no write.
110
+
95
111
  # Link/unlink a work item, add/remove reviewers
96
112
  azdo pr work-items link 1234 --pr-number 64
97
113
  azdo pr work-items unlink 1234 --pr-number 64
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`
@@ -4083,7 +4105,12 @@ function parsePositivePrNumber(raw) {
4083
4105
  const n = Number.parseInt(raw, 10);
4084
4106
  return Number.isFinite(n) && n > 0 ? n : null;
4085
4107
  }
4086
- var PR_NUMBER_HELP = "target the pull request with this numeric id, instead of the current branch's PR. When omitted, the CLI auto-detects the pull request whose source branch equals refs/heads/<current branch> in the Azure DevOps repository identified by the origin remote; if zero or more than one open PR matches, the command fails with a message naming the searched branch.";
4108
+ function prNumberHelp(branchStatus = "active") {
4109
+ const matched = branchStatus === "abandoned" ? "abandoned" : "open";
4110
+ return `target the pull request with this numeric id, instead of the current branch's PR. When omitted, the CLI auto-detects the pull request whose source branch equals refs/heads/<current branch> in the Azure DevOps repository identified by the origin remote; if zero or more than one ${matched} PR matches, the command fails with a message naming the searched branch.`;
4111
+ }
4112
+ var PR_NUMBER_HELP = prNumberHelp();
4113
+ var PR_NUMBER_HELP_ABANDONED = prNumberHelp("abandoned");
4087
4114
  var PR_REPO_HELP = 'Azure DevOps repository name; defaults to the repository of the git "origin" remote';
4088
4115
  function configureUnwrappedHelp(command) {
4089
4116
  return command.configureHelp({ helpWidth: 1e3 });
@@ -4101,12 +4128,40 @@ function autoDetectMultiMatch(branch, ids) {
4101
4128
  const idList = ids.map((id) => `#${id}`).join(", ");
4102
4129
  return `Multiple open pull requests match branch ${branch}: ${idList}. Re-run with --pr-number to choose.`;
4103
4130
  }
4131
+ function abandonedAutoDetectZeroMatch(branch) {
4132
+ return `No abandoned pull request matches branch ${branch}. Pass --pr-number to target a specific PR.`;
4133
+ }
4134
+ function abandonedAutoDetectMultiMatch(branch, ids) {
4135
+ const idList = ids.map((id) => `#${id}`).join(", ");
4136
+ return `Multiple abandoned pull requests match branch ${branch}: ${idList}. Re-run with --pr-number to choose.`;
4137
+ }
4104
4138
  function writeContractError(line) {
4105
4139
  process.stderr.write(`${line}
4106
4140
  `);
4107
4141
  process.exitCode = 1;
4108
4142
  }
4109
4143
  var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
4144
+ var STDIN_PATH = "-";
4145
+ function readTextSource(file) {
4146
+ if (file === STDIN_PATH) {
4147
+ try {
4148
+ return readFileSync5(0, "utf-8");
4149
+ } catch {
4150
+ writeError("Cannot read standard input.");
4151
+ return null;
4152
+ }
4153
+ }
4154
+ if (!existsSync5(file)) {
4155
+ writeError(`File not found: ${file}`);
4156
+ return null;
4157
+ }
4158
+ try {
4159
+ return readFileSync5(file, "utf-8");
4160
+ } catch {
4161
+ writeError(`Cannot read file: ${file}`);
4162
+ return null;
4163
+ }
4164
+ }
4110
4165
  function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4111
4166
  if (inline !== void 0 && file !== void 0) {
4112
4167
  writeError("Cannot specify both inline text and --file.");
@@ -4114,16 +4169,11 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4114
4169
  }
4115
4170
  let body;
4116
4171
  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}`);
4172
+ const read = readTextSource(file);
4173
+ if (read === null) {
4125
4174
  return null;
4126
4175
  }
4176
+ body = read;
4127
4177
  } else if (inline !== void 0) {
4128
4178
  body = inline;
4129
4179
  } else {
@@ -4137,6 +4187,33 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
4137
4187
  }
4138
4188
  return trimmed;
4139
4189
  }
4190
+ function resolveOptionalTextInput(inline, file, flag) {
4191
+ if (inline !== void 0 && file !== void 0) {
4192
+ writeError(`Cannot specify both --${flag} and --${flag}-file.`);
4193
+ return null;
4194
+ }
4195
+ if (inline === void 0 && file === void 0) {
4196
+ return void 0;
4197
+ }
4198
+ const label = flag === "title" ? "Title" : "Description";
4199
+ const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
4200
+ let value;
4201
+ if (file !== void 0) {
4202
+ const read = readTextSource(file);
4203
+ if (read === null) {
4204
+ return null;
4205
+ }
4206
+ value = read;
4207
+ } else {
4208
+ value = inline;
4209
+ }
4210
+ const trimmed = value.trim();
4211
+ if (trimmed === "") {
4212
+ writeError(emptyMessage);
4213
+ return null;
4214
+ }
4215
+ return trimmed;
4216
+ }
4140
4217
  function parseNonNegativeInt(raw) {
4141
4218
  if (!/^\d+$/.test(raw)) {
4142
4219
  return null;
@@ -4392,11 +4469,37 @@ function handlePrOpenError(err, context) {
4392
4469
  }
4393
4470
  handlePrCommandError(err, context, "write");
4394
4471
  }
4472
+ function resolveOpenDescription(inline, file) {
4473
+ if (inline !== void 0 && file !== void 0) {
4474
+ writeError("Cannot specify both --description and --description-file.");
4475
+ return null;
4476
+ }
4477
+ if (file !== void 0) {
4478
+ const fromFile = readTextSource(file);
4479
+ if (fromFile === null) {
4480
+ return null;
4481
+ }
4482
+ const trimmed = fromFile.trim();
4483
+ if (trimmed === "") {
4484
+ writeError("Description must not be empty. Pass the text inline or use --description-file <path>.");
4485
+ return null;
4486
+ }
4487
+ return trimmed;
4488
+ }
4489
+ const trimmedInline = inline?.trim();
4490
+ if (trimmedInline === void 0 || trimmedInline === "") {
4491
+ return void 0;
4492
+ }
4493
+ return trimmedInline;
4494
+ }
4395
4495
  function createPrOpenCommand() {
4396
4496
  const command = new Command12("open");
4397
4497
  withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
4398
4498
  "--description <description>",
4399
4499
  "pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
4500
+ ).option(
4501
+ "--description-file <path>",
4502
+ 'read the description from a UTF-8 file instead of --description; "-" reads standard input'
4400
4503
  ).option("--json", "output JSON").action(async (options) => {
4401
4504
  validateOrgProjectPair(options);
4402
4505
  const title = options.title?.trim();
@@ -4404,8 +4507,10 @@ function createPrOpenCommand() {
4404
4507
  writeError("--title is required for pull request creation.");
4405
4508
  return;
4406
4509
  }
4407
- const trimmedDescription = options.description?.trim();
4408
- const description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
4510
+ const description = resolveOpenDescription(options.description, options.descriptionFile);
4511
+ if (description === null) {
4512
+ return;
4513
+ }
4409
4514
  let context;
4410
4515
  try {
4411
4516
  const resolved = await resolvePrCommandContext(options);
@@ -4445,6 +4550,222 @@ ${result.pullRequest.url ?? "\u2014"}
4445
4550
  });
4446
4551
  return command;
4447
4552
  }
4553
+ function handlePrUpdateError(err, context) {
4554
+ const message = err instanceof Error ? err.message : "";
4555
+ if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
4556
+ writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
4557
+ return;
4558
+ }
4559
+ handlePrCommandError(err, context, "write");
4560
+ }
4561
+ function joinFieldNames(fields) {
4562
+ return fields.join(" and ");
4563
+ }
4564
+ function resolveRequestedFields(options) {
4565
+ if (options.titleFile === STDIN_PATH && options.descriptionFile === STDIN_PATH) {
4566
+ writeError('Cannot read standard input twice; only one of --title-file and --description-file may be "-".');
4567
+ return null;
4568
+ }
4569
+ const title = resolveOptionalTextInput(options.title, options.titleFile, "title");
4570
+ if (title === null) {
4571
+ return null;
4572
+ }
4573
+ const description = resolveOptionalTextInput(options.description, options.descriptionFile, "description");
4574
+ if (description === null) {
4575
+ return null;
4576
+ }
4577
+ if (title === void 0 && description === void 0) {
4578
+ writeError("pr update requires at least one of --title, --title-file, --description or --description-file.");
4579
+ return null;
4580
+ }
4581
+ return { title, description };
4582
+ }
4583
+ function diffRequestedFields(requested, current) {
4584
+ const fields = {};
4585
+ const updatedFields = [];
4586
+ const requestedFields = [];
4587
+ if (requested.title !== void 0) {
4588
+ requestedFields.push("title");
4589
+ if (requested.title !== current.title) {
4590
+ fields.title = requested.title;
4591
+ updatedFields.push("title");
4592
+ }
4593
+ }
4594
+ if (requested.description !== void 0) {
4595
+ requestedFields.push("description");
4596
+ if (requested.description !== (current.description ?? "")) {
4597
+ fields.description = requested.description;
4598
+ updatedFields.push("description");
4599
+ }
4600
+ }
4601
+ return { fields, updatedFields, requestedFields };
4602
+ }
4603
+ async function runPrUpdate(options) {
4604
+ const requested = resolveRequestedFields(options);
4605
+ if (requested === null) {
4606
+ return;
4607
+ }
4608
+ let context;
4609
+ try {
4610
+ const target = await resolvePullRequestTarget(options, {
4611
+ onContextResolved: (resolved) => {
4612
+ context = resolved;
4613
+ }
4614
+ });
4615
+ if (target === null) {
4616
+ return;
4617
+ }
4618
+ const current = target.pullRequest;
4619
+ const { fields, updatedFields, requestedFields } = diffRequestedFields(requested, current);
4620
+ if (updatedFields.length === 0) {
4621
+ const noopResult = {
4622
+ pullRequestId: current.id,
4623
+ title: current.title,
4624
+ description: current.description ?? null,
4625
+ url: current.url,
4626
+ noop: true,
4627
+ updatedFields: []
4628
+ };
4629
+ if (options.json) {
4630
+ process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
4631
+ `);
4632
+ return;
4633
+ }
4634
+ process.stdout.write(
4635
+ `Pull request #${current.id} already has the requested ${joinFieldNames(requestedFields)}; nothing to update.
4636
+ `
4637
+ );
4638
+ return;
4639
+ }
4640
+ const updated = await updatePullRequest(target.context, target.repo, target.pat, current.id, fields);
4641
+ const result = {
4642
+ pullRequestId: updated.id,
4643
+ title: updated.title,
4644
+ description: updated.description ?? null,
4645
+ url: updated.url,
4646
+ noop: false,
4647
+ updatedFields
4648
+ };
4649
+ if (options.json) {
4650
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4651
+ `);
4652
+ return;
4653
+ }
4654
+ process.stdout.write(
4655
+ `Updated pull request #${updated.id} (${updatedFields.join(", ")}).
4656
+ ${updated.url ?? "\u2014"}
4657
+ `
4658
+ );
4659
+ } catch (err) {
4660
+ handlePrUpdateError(err, context);
4661
+ }
4662
+ }
4663
+ function createPrUpdateCommand() {
4664
+ const command = new Command12("update");
4665
+ 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(
4666
+ "--description <description>",
4667
+ 'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
4668
+ ).option("--description-file <path>", 'read the new description from a UTF-8 file; "-" reads standard input').option("--json", "output JSON").action(async (_options, command2) => {
4669
+ await runPrUpdate(mergedPrOptions(command2));
4670
+ });
4671
+ return command;
4672
+ }
4673
+ var PR_STATUS_CHANGES = {
4674
+ abandon: {
4675
+ target: "abandoned",
4676
+ search: "active",
4677
+ verb: "abandoned",
4678
+ applied: "Abandoned",
4679
+ refusal: "abandoned"
4680
+ },
4681
+ reactivate: {
4682
+ target: "active",
4683
+ search: "abandoned",
4684
+ verb: "active",
4685
+ applied: "Reactivated",
4686
+ refusal: "reactivated"
4687
+ }
4688
+ };
4689
+ var COMPLETED_STATUS = "completed";
4690
+ async function runPrStatusChange(options, direction) {
4691
+ const change = PR_STATUS_CHANGES[direction];
4692
+ let context;
4693
+ try {
4694
+ const target = await resolvePullRequestTarget(options, {
4695
+ branchStatus: change.search,
4696
+ onContextResolved: (resolved) => {
4697
+ context = resolved;
4698
+ }
4699
+ });
4700
+ if (target === null) {
4701
+ return;
4702
+ }
4703
+ const current = target.pullRequest;
4704
+ if (current.status === COMPLETED_STATUS) {
4705
+ writeError(
4706
+ `Pull request #${current.id} is completed and cannot be ${change.refusal}. A completed pull request is final; revert it with a new pull request instead.`
4707
+ );
4708
+ return;
4709
+ }
4710
+ if (current.status === change.target) {
4711
+ const noopResult = {
4712
+ pullRequestId: current.id,
4713
+ title: current.title,
4714
+ status: current.status,
4715
+ previousStatus: current.status,
4716
+ url: current.url,
4717
+ noop: true
4718
+ };
4719
+ if (options.json) {
4720
+ process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
4721
+ `);
4722
+ return;
4723
+ }
4724
+ process.stdout.write(`Pull request #${current.id} is already ${change.verb}; nothing to do.
4725
+ `);
4726
+ return;
4727
+ }
4728
+ const updated = await updatePullRequest(target.context, target.repo, target.pat, current.id, {
4729
+ status: change.target
4730
+ });
4731
+ const result = {
4732
+ pullRequestId: updated.id,
4733
+ title: updated.title,
4734
+ status: updated.status,
4735
+ previousStatus: current.status,
4736
+ url: updated.url,
4737
+ noop: false
4738
+ };
4739
+ if (options.json) {
4740
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4741
+ `);
4742
+ return;
4743
+ }
4744
+ process.stdout.write(`${change.applied} pull request #${updated.id} (${updated.title}).
4745
+ ${updated.url ?? "\u2014"}
4746
+ `);
4747
+ } catch (err) {
4748
+ handlePrCommandError(err, context, "write");
4749
+ }
4750
+ }
4751
+ function createPrAbandonCommand() {
4752
+ const command = new Command12("abandon");
4753
+ withCommonPrOptions(configureUnwrappedHelp(command)).alias("close").description(
4754
+ '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.'
4755
+ ).option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
4756
+ await runPrStatusChange(mergedPrOptions(command2), "abandon");
4757
+ });
4758
+ return command;
4759
+ }
4760
+ function createPrReactivateCommand() {
4761
+ const command = new Command12("reactivate");
4762
+ withCommonPrOptions(configureUnwrappedHelp(command)).description(
4763
+ "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."
4764
+ ).option("--pr-number <N>", PR_NUMBER_HELP_ABANDONED).option("--json", "output JSON").action(async (_options, command2) => {
4765
+ await runPrStatusChange(mergedPrOptions(command2), "reactivate");
4766
+ });
4767
+ return command;
4768
+ }
4448
4769
  function createPrCommentsCommand() {
4449
4770
  const command = new Command12("comments");
4450
4771
  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) => {
@@ -4560,52 +4881,75 @@ function createPrCommentsCommand() {
4560
4881
  command.addCommand(createPrCommentsEditCommand());
4561
4882
  return command;
4562
4883
  }
4563
- async function resolvePullRequestTarget(options) {
4564
- validateOrgProjectPair(options);
4565
- let explicitPrId = null;
4566
- if (options.prNumber !== void 0) {
4567
- explicitPrId = parsePositivePrNumber(options.prNumber);
4568
- if (explicitPrId === null) {
4569
- writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
4570
- return null;
4571
- }
4884
+ function parseTargetPrNumber(options) {
4885
+ if (options.prNumber === void 0) {
4886
+ return "none";
4572
4887
  }
4573
- const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
4574
- let pullRequest;
4575
- if (explicitPrId !== null) {
4576
- try {
4577
- pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
4578
- } catch (err) {
4579
- if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
4580
- writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`, EXIT_NOT_FOUND);
4581
- return null;
4582
- }
4583
- throw err;
4584
- }
4585
- } else {
4586
- const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
4587
- status: "active"
4588
- });
4589
- if (pullRequests.length === 0) {
4590
- writeContractError(autoDetectZeroMatch(resolved.branch));
4591
- return null;
4592
- }
4593
- if (pullRequests.length > 1) {
4594
- writeContractError(autoDetectMultiMatch(resolved.branch, pullRequests.map((pr) => pr.id)));
4888
+ const parsed = parsePositivePrNumber(options.prNumber);
4889
+ if (parsed === null) {
4890
+ writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
4891
+ return "invalid";
4892
+ }
4893
+ return parsed;
4894
+ }
4895
+ async function fetchTargetById(resolved, prId) {
4896
+ try {
4897
+ return await getPullRequestById(resolved.context, resolved.repo, resolved.pat, prId);
4898
+ } catch (err) {
4899
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
4900
+ writeError(
4901
+ `Pull request #${prId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`,
4902
+ EXIT_NOT_FOUND
4903
+ );
4595
4904
  return null;
4596
4905
  }
4597
- pullRequest = pullRequests[0];
4906
+ throw err;
4907
+ }
4908
+ }
4909
+ async function findBranchPullRequest(resolved, branch, branchStatus) {
4910
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch, {
4911
+ status: branchStatus
4912
+ });
4913
+ const abandoned = branchStatus === "abandoned";
4914
+ if (pullRequests.length === 0) {
4915
+ writeContractError(abandoned ? abandonedAutoDetectZeroMatch(branch) : autoDetectZeroMatch(branch));
4916
+ return null;
4917
+ }
4918
+ if (pullRequests.length > 1) {
4919
+ const ids = pullRequests.map((pr) => pr.id);
4920
+ writeContractError(
4921
+ abandoned ? abandonedAutoDetectMultiMatch(branch, ids) : autoDetectMultiMatch(branch, ids)
4922
+ );
4923
+ return null;
4924
+ }
4925
+ return pullRequests[0];
4926
+ }
4927
+ async function resolvePullRequestTarget(options, {
4928
+ branchStatus = "active",
4929
+ onContextResolved
4930
+ } = {}) {
4931
+ validateOrgProjectPair(options);
4932
+ const explicitPrId = parseTargetPrNumber(options);
4933
+ if (explicitPrId === "invalid") {
4934
+ return null;
4935
+ }
4936
+ const byBranch = explicitPrId === "none";
4937
+ const resolved = await resolvePrCommandContext(options, { requireBranch: byBranch });
4938
+ onContextResolved?.(resolved.context);
4939
+ const pullRequest = byBranch ? await findBranchPullRequest(resolved, resolved.branch, branchStatus) : await fetchTargetById(resolved, explicitPrId);
4940
+ if (pullRequest === null) {
4941
+ return null;
4598
4942
  }
4599
4943
  return { context: resolved.context, repo: resolved.repo, pat: resolved.pat, pullRequest };
4600
4944
  }
4601
- async function resolveThreadTarget(threadIdRaw, options) {
4945
+ async function resolveThreadTarget(threadIdRaw, options, { onContextResolved } = {}) {
4602
4946
  const threadId = parsePositivePrNumber(threadIdRaw);
4603
4947
  if (threadId === null) {
4604
4948
  validateOrgProjectPair(options);
4605
4949
  writeError(`Invalid thread id "${threadIdRaw}"; expected a positive integer.`);
4606
4950
  return null;
4607
4951
  }
4608
- const target = await resolvePullRequestTarget(options);
4952
+ const target = await resolvePullRequestTarget(options, { onContextResolved });
4609
4953
  if (target === null) {
4610
4954
  return null;
4611
4955
  }
@@ -4614,11 +4958,14 @@ async function resolveThreadTarget(threadIdRaw, options) {
4614
4958
  async function runThreadStateChange(threadIdRaw, options, direction) {
4615
4959
  let context;
4616
4960
  try {
4617
- const target = await resolveThreadTarget(threadIdRaw, options);
4961
+ const target = await resolveThreadTarget(threadIdRaw, options, {
4962
+ onContextResolved: (resolved) => {
4963
+ context = resolved;
4964
+ }
4965
+ });
4618
4966
  if (target === null) {
4619
4967
  return;
4620
4968
  }
4621
- context = target.context;
4622
4969
  const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
4623
4970
  const thread = threads.find((t) => t.id === target.threadId);
4624
4971
  if (!thread) {
@@ -4691,11 +5038,14 @@ async function runCommentReply(threadIdRaw, text, options) {
4691
5038
  if (trimmedText === null) {
4692
5039
  return;
4693
5040
  }
4694
- const target = await resolveThreadTarget(threadIdRaw, options);
5041
+ const target = await resolveThreadTarget(threadIdRaw, options, {
5042
+ onContextResolved: (resolved) => {
5043
+ context = resolved;
5044
+ }
5045
+ });
4695
5046
  if (target === null) {
4696
5047
  return;
4697
5048
  }
4698
- context = target.context;
4699
5049
  const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
4700
5050
  const thread = threads.find((t) => t.id === target.threadId);
4701
5051
  if (!thread) {
@@ -4769,11 +5119,14 @@ async function runCommentAdd(text, options) {
4769
5119
  }
4770
5120
  status2 = match;
4771
5121
  }
4772
- const target = await resolvePullRequestTarget(options);
5122
+ const target = await resolvePullRequestTarget(options, {
5123
+ onContextResolved: (resolved) => {
5124
+ context = resolved;
5125
+ }
5126
+ });
4773
5127
  if (target === null) {
4774
5128
  return;
4775
5129
  }
4776
- context = target.context;
4777
5130
  if (options.dryRun === true) {
4778
5131
  const dryResult = {
4779
5132
  pullRequestId: target.pullRequest.id,
@@ -4912,11 +5265,14 @@ async function runCommentEdit(threadIdRaw, text, options) {
4912
5265
  return;
4913
5266
  }
4914
5267
  }
4915
- const target = await resolveThreadTarget(threadIdRaw, options);
5268
+ const target = await resolveThreadTarget(threadIdRaw, options, {
5269
+ onContextResolved: (resolved) => {
5270
+ context = resolved;
5271
+ }
5272
+ });
4916
5273
  if (target === null) {
4917
5274
  return;
4918
5275
  }
4919
- context = target.context;
4920
5276
  const thread = await fetchThreadForEdit(target);
4921
5277
  if (thread === null) {
4922
5278
  return;
@@ -5051,11 +5407,14 @@ async function runWorkItemLinkChange(workItemIdRaw, options, direction) {
5051
5407
  writeError(`Invalid work item id "${workItemIdRaw}"; expected a positive integer.`);
5052
5408
  return;
5053
5409
  }
5054
- const target = await resolvePullRequestTarget(options);
5410
+ const target = await resolvePullRequestTarget(options, {
5411
+ onContextResolved: (resolved) => {
5412
+ context = resolved;
5413
+ }
5414
+ });
5055
5415
  if (target === null) {
5056
5416
  return;
5057
5417
  }
5058
- context = target.context;
5059
5418
  let outcome;
5060
5419
  try {
5061
5420
  outcome = direction === "link" ? await linkWorkItemToPullRequest(target.context, target.repo, target.pat, target.pullRequest.id, workItemId) : await unlinkWorkItemFromPullRequest(target.context, target.repo, target.pat, target.pullRequest.id, workItemId);
@@ -5110,11 +5469,14 @@ function createPrWorkItemsCommand() {
5110
5469
  async function runReviewerAdd(reviewer, options) {
5111
5470
  let context;
5112
5471
  try {
5113
- const target = await resolvePullRequestTarget(options);
5472
+ const target = await resolvePullRequestTarget(options, {
5473
+ onContextResolved: (resolved) => {
5474
+ context = resolved;
5475
+ }
5476
+ });
5114
5477
  if (target === null) {
5115
5478
  return;
5116
5479
  }
5117
- context = target.context;
5118
5480
  let identity;
5119
5481
  try {
5120
5482
  identity = await resolveReviewerIdentity(target.context.org, target.pat, reviewer);
@@ -5165,11 +5527,14 @@ async function runReviewerAdd(reviewer, options) {
5165
5527
  async function runReviewerRemove(reviewer, options) {
5166
5528
  let context;
5167
5529
  try {
5168
- const target = await resolvePullRequestTarget(options);
5530
+ const target = await resolvePullRequestTarget(options, {
5531
+ onContextResolved: (resolved) => {
5532
+ context = resolved;
5533
+ }
5534
+ });
5169
5535
  if (target === null) {
5170
5536
  return;
5171
5537
  }
5172
- context = target.context;
5173
5538
  let identity;
5174
5539
  try {
5175
5540
  identity = await resolveReviewerIdentity(target.context.org, target.pat, reviewer);
@@ -5224,6 +5589,9 @@ function createPrCommand() {
5224
5589
  command.addCommand(createPrListCommand());
5225
5590
  command.addCommand(createPrStatusCommand());
5226
5591
  command.addCommand(createPrOpenCommand());
5592
+ command.addCommand(createPrUpdateCommand());
5593
+ command.addCommand(createPrAbandonCommand());
5594
+ command.addCommand(createPrReactivateCommand());
5227
5595
  command.addCommand(createPrCommentsCommand());
5228
5596
  command.addCommand(createPrCommentResolveCommand());
5229
5597
  command.addCommand(createPrCommentReopenCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.19.0-develop.703",
3
+ "version": "0.19.0-develop.717",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {