azdo-cli 0.5.0-017-pr-comments-threads.242 → 0.5.0-017-pr-comments-threads.246

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 +215 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2396,6 +2396,14 @@ function mapThread(thread) {
2396
2396
  comments
2397
2397
  };
2398
2398
  }
2399
+ function toActiveCommentThread(thread) {
2400
+ return {
2401
+ id: thread.id,
2402
+ status: thread.status,
2403
+ threadContext: thread.threadContext?.filePath ?? null,
2404
+ comments: thread.comments.map(mapComment).filter((comment) => comment !== null)
2405
+ };
2406
+ }
2399
2407
  var RESOLVED_THREAD_STATUSES = /* @__PURE__ */ new Set(["fixed", "wontFix", "closed", "byDesign"]);
2400
2408
  function isThreadResolved(status) {
2401
2409
  return RESOLVED_THREAD_STATUSES.has(status);
@@ -2406,6 +2414,31 @@ async function readJsonResponse(response) {
2406
2414
  }
2407
2415
  return response.json();
2408
2416
  }
2417
+ async function patchThreadStatus(context, repo, pat, prId, threadId, status) {
2418
+ const url = new URL(
2419
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}`
2420
+ );
2421
+ url.searchParams.set("api-version", "7.1");
2422
+ const response = await fetchWithErrors(url.toString(), {
2423
+ method: "PATCH",
2424
+ headers: {
2425
+ ...authHeaders(pat),
2426
+ "Content-Type": "application/json"
2427
+ },
2428
+ body: JSON.stringify({ status })
2429
+ });
2430
+ const data = await readJsonResponse(response);
2431
+ return toActiveCommentThread(data);
2432
+ }
2433
+ async function getPullRequestById(context, repo, pat, prId) {
2434
+ const url = new URL(
2435
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}`
2436
+ );
2437
+ url.searchParams.set("api-version", "7.1");
2438
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
2439
+ const data = await readJsonResponse(response);
2440
+ return mapPullRequest(repo, data);
2441
+ }
2409
2442
  async function listPullRequests(context, repo, pat, sourceBranch, opts) {
2410
2443
  const response = await fetchWithErrors(
2411
2444
  buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
@@ -2475,6 +2508,13 @@ async function getPullRequestThreads(context, repo, pat, prId) {
2475
2508
  }
2476
2509
 
2477
2510
  // src/commands/pr.ts
2511
+ function parsePositivePrNumber(raw) {
2512
+ if (!/^\d+$/.test(raw)) {
2513
+ return null;
2514
+ }
2515
+ const n = Number.parseInt(raw, 10);
2516
+ return Number.isFinite(n) && n > 0 ? n : null;
2517
+ }
2478
2518
  function formatBranchName(refName) {
2479
2519
  return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
2480
2520
  }
@@ -2542,10 +2582,20 @@ function formatThreads(prId, title, threads) {
2542
2582
  }
2543
2583
  return lines.join("\n");
2544
2584
  }
2545
- async function resolvePrCommandContext(options) {
2585
+ async function resolvePrCommandContext(options, resolveOpts = {}) {
2586
+ const requireBranch = resolveOpts.requireBranch ?? true;
2546
2587
  const context = resolveContext(options);
2547
2588
  const repo = detectRepoName();
2548
- const branch = getCurrentBranch();
2589
+ let branch;
2590
+ if (requireBranch) {
2591
+ branch = getCurrentBranch();
2592
+ } else {
2593
+ try {
2594
+ branch = getCurrentBranch();
2595
+ } catch {
2596
+ branch = null;
2597
+ }
2598
+ }
2549
2599
  const credential = await requirePat(context.org);
2550
2600
  return {
2551
2601
  context,
@@ -2562,15 +2612,15 @@ function createPrStatusCommand() {
2562
2612
  try {
2563
2613
  const resolved = await resolvePrCommandContext(options);
2564
2614
  context = resolved.context;
2565
- const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch);
2615
+ const branch = resolved.branch;
2616
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
2566
2617
  const pullRequestsWithChecks = await Promise.all(
2567
2618
  pullRequests.map(async (pullRequest) => ({
2568
2619
  ...pullRequest,
2569
2620
  checks: await getPullRequestChecks(resolved.context, resolved.repo, resolved.pat, pullRequest.id)
2570
2621
  }))
2571
2622
  );
2572
- const { branch, repo } = resolved;
2573
- const result = { branch, repository: repo, pullRequests: pullRequestsWithChecks };
2623
+ const result = { branch, repository: resolved.repo, pullRequests: pullRequestsWithChecks };
2574
2624
  if (options.json) {
2575
2625
  process.stdout.write(`${JSON.stringify(result, null, 2)}
2576
2626
  `);
@@ -2611,11 +2661,12 @@ function createPrOpenCommand() {
2611
2661
  writeError("Pull request creation requires a source branch other than develop.");
2612
2662
  return;
2613
2663
  }
2664
+ const openBranch = resolved.branch;
2614
2665
  const result = await openPullRequest(
2615
2666
  resolved.context,
2616
2667
  resolved.repo,
2617
2668
  resolved.pat,
2618
- resolved.branch,
2669
+ openBranch,
2619
2670
  title,
2620
2671
  description
2621
2672
  );
@@ -2648,28 +2699,52 @@ ${result.pullRequest.url ?? "\u2014"}
2648
2699
  }
2649
2700
  function createPrCommentsCommand() {
2650
2701
  const command = new Command12("comments");
2651
- command.description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
2702
+ command.description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
2652
2703
  validateOrgProjectPair(options);
2653
2704
  let context;
2654
- try {
2655
- const resolved = await resolvePrCommandContext(options);
2656
- context = resolved.context;
2657
- const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
2658
- status: "active"
2659
- });
2660
- if (pullRequests.length === 0) {
2661
- writeError(`No active pull request found for branch ${resolved.branch}.`);
2705
+ let explicitPrId = null;
2706
+ if (options.prNumber !== void 0) {
2707
+ explicitPrId = parsePositivePrNumber(options.prNumber);
2708
+ if (explicitPrId === null) {
2709
+ writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
2662
2710
  return;
2663
2711
  }
2664
- if (pullRequests.length > 1) {
2665
- const ids = pullRequests.map((pullRequest2) => `#${pullRequest2.id}`).join(", ");
2666
- writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
2667
- return;
2712
+ }
2713
+ try {
2714
+ const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
2715
+ context = resolved.context;
2716
+ let pullRequest;
2717
+ let branchLabel;
2718
+ if (explicitPrId !== null) {
2719
+ try {
2720
+ pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
2721
+ } catch (err) {
2722
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
2723
+ writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
2724
+ return;
2725
+ }
2726
+ throw err;
2727
+ }
2728
+ branchLabel = resolved.branch ?? pullRequest.sourceRefName;
2729
+ } else {
2730
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
2731
+ status: "active"
2732
+ });
2733
+ if (pullRequests.length === 0) {
2734
+ writeError(`No active pull request found for branch ${resolved.branch}.`);
2735
+ return;
2736
+ }
2737
+ if (pullRequests.length > 1) {
2738
+ const ids = pullRequests.map((pr) => `#${pr.id}`).join(", ");
2739
+ writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
2740
+ return;
2741
+ }
2742
+ pullRequest = pullRequests[0];
2743
+ branchLabel = resolved.branch;
2668
2744
  }
2669
- const pullRequest = pullRequests[0];
2670
2745
  const allThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
2671
2746
  const threads = options.hideResolved ? allThreads.filter((thread) => !isThreadResolved(thread.status)) : allThreads;
2672
- const result = { branch: resolved.branch, pullRequest, threads };
2747
+ const result = { branch: branchLabel, pullRequest, threads };
2673
2748
  if (options.json) {
2674
2749
  process.stdout.write(`${JSON.stringify(result, null, 2)}
2675
2750
  `);
@@ -2688,12 +2763,131 @@ function createPrCommentsCommand() {
2688
2763
  });
2689
2764
  return command;
2690
2765
  }
2766
+ async function resolveThreadTarget(threadIdRaw, options) {
2767
+ validateOrgProjectPair(options);
2768
+ const threadId = parsePositivePrNumber(threadIdRaw);
2769
+ if (threadId === null) {
2770
+ writeError(`Invalid thread id "${threadIdRaw}"; expected a positive integer.`);
2771
+ return null;
2772
+ }
2773
+ let explicitPrId = null;
2774
+ if (options.prNumber !== void 0) {
2775
+ explicitPrId = parsePositivePrNumber(options.prNumber);
2776
+ if (explicitPrId === null) {
2777
+ writeError(`Invalid --pr-number "${options.prNumber}"; expected a positive integer.`);
2778
+ return null;
2779
+ }
2780
+ }
2781
+ const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
2782
+ let pullRequest;
2783
+ if (explicitPrId !== null) {
2784
+ try {
2785
+ pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
2786
+ } catch (err) {
2787
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
2788
+ writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
2789
+ return null;
2790
+ }
2791
+ throw err;
2792
+ }
2793
+ } else {
2794
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
2795
+ status: "active"
2796
+ });
2797
+ if (pullRequests.length === 0) {
2798
+ writeError(`No active pull request found for branch ${resolved.branch}.`);
2799
+ return null;
2800
+ }
2801
+ if (pullRequests.length > 1) {
2802
+ const ids = pullRequests.map((pr) => `#${pr.id}`).join(", ");
2803
+ writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
2804
+ return null;
2805
+ }
2806
+ pullRequest = pullRequests[0];
2807
+ }
2808
+ return { context: resolved.context, repo: resolved.repo, pat: resolved.pat, pullRequest, threadId };
2809
+ }
2810
+ async function runThreadStateChange(threadIdRaw, options, direction) {
2811
+ let context;
2812
+ try {
2813
+ const target = await resolveThreadTarget(threadIdRaw, options);
2814
+ if (target === null) {
2815
+ return;
2816
+ }
2817
+ context = target.context;
2818
+ const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
2819
+ const thread = threads.find((t) => t.id === target.threadId);
2820
+ if (!thread) {
2821
+ writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`);
2822
+ return;
2823
+ }
2824
+ const alreadyInTargetState = direction === "resolve" ? isThreadResolved(thread.status) : !isThreadResolved(thread.status);
2825
+ const targetStatus = direction === "resolve" ? "fixed" : "active";
2826
+ if (alreadyInTargetState) {
2827
+ const humanLabel = direction === "resolve" ? "resolved" : "active";
2828
+ const noopResult = {
2829
+ pullRequestId: target.pullRequest.id,
2830
+ threadId: target.threadId,
2831
+ status: targetStatus,
2832
+ noop: true
2833
+ };
2834
+ if (options.json) {
2835
+ process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
2836
+ `);
2837
+ return;
2838
+ }
2839
+ process.stdout.write(`Thread #${target.threadId} is already ${humanLabel} on pull request #${target.pullRequest.id}.
2840
+ `);
2841
+ return;
2842
+ }
2843
+ const updated = await patchThreadStatus(
2844
+ target.context,
2845
+ target.repo,
2846
+ target.pat,
2847
+ target.pullRequest.id,
2848
+ target.threadId,
2849
+ targetStatus
2850
+ );
2851
+ const result = {
2852
+ pullRequestId: target.pullRequest.id,
2853
+ threadId: target.threadId,
2854
+ status: targetStatus,
2855
+ noop: false
2856
+ };
2857
+ if (options.json) {
2858
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2859
+ `);
2860
+ return;
2861
+ }
2862
+ const verb = direction === "resolve" ? "resolved" : "reopened";
2863
+ process.stdout.write(`Thread #${target.threadId} ${verb} on pull request #${target.pullRequest.id} (status: ${updated.status}).
2864
+ `);
2865
+ } catch (err) {
2866
+ handlePrCommandError(err, context, "write");
2867
+ }
2868
+ }
2869
+ function createPrCommentResolveCommand() {
2870
+ const command = new Command12("comment-resolve");
2871
+ command.description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--json", "output JSON").action(async (threadIdRaw, options) => {
2872
+ await runThreadStateChange(threadIdRaw, options, "resolve");
2873
+ });
2874
+ return command;
2875
+ }
2876
+ function createPrCommentReopenCommand() {
2877
+ const command = new Command12("comment-reopen");
2878
+ command.description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", "target the pull request with this numeric id, instead of the current branch's PR").option("--json", "output JSON").action(async (threadIdRaw, options) => {
2879
+ await runThreadStateChange(threadIdRaw, options, "reopen");
2880
+ });
2881
+ return command;
2882
+ }
2691
2883
  function createPrCommand() {
2692
2884
  const command = new Command12("pr");
2693
2885
  command.description("Manage Azure DevOps pull requests");
2694
2886
  command.addCommand(createPrStatusCommand());
2695
2887
  command.addCommand(createPrOpenCommand());
2696
2888
  command.addCommand(createPrCommentsCommand());
2889
+ command.addCommand(createPrCommentResolveCommand());
2890
+ command.addCommand(createPrCommentReopenCommand());
2697
2891
  return command;
2698
2892
  }
2699
2893
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.5.0-017-pr-comments-threads.242",
3
+ "version": "0.5.0-017-pr-comments-threads.246",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {