azdo-cli 0.15.0-develop.593 → 0.15.0-develop.604

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 +15 -1
  2. package/dist/index.js +598 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -54,20 +54,34 @@ azdo upsert --type "User Story" --content $'---\nTitle: Improve markdown import
54
54
  azdo comments list 12345
55
55
  azdo comments add 12345 "Investigating the root cause now."
56
56
 
57
+ # Find a pull request — one API call, any branch
58
+ azdo pr list # active PRs in the repository
59
+ azdo pr list --branch feature/x --json # id, title, source/target, author, url, description
60
+ azdo pr list --status all --top 50
61
+
57
62
  # PR comment threads — list, filter, target by number, resolve or reopen
58
63
  azdo pr comments # active-branch PR; code-anchored threads show file:line
59
64
  azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
60
65
  azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
61
66
  azdo pr comments --code-related-only # only file/line-anchored threads
67
+ azdo pr comments --exclude-system --max-chars 500 # human comments only, truncated
68
+ azdo pr comments --thread 148 # a single thread, by id (selector: exit 1 if absent)
69
+ azdo pr comments --contains '"kind":"review-plan"' # threads holding a literal substring
62
70
  azdo pr status # PR checks (status + branch policies + pipeline builds) + code-comment counts
63
71
  azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
64
72
  azdo pr comment-reopen 17 --pr-number 64
65
73
 
66
- # Reply to a PR comment thread
74
+ # Write to a PR new thread, in-place edit, reply
75
+ azdo pr comments add --file plan.md --pr-number 64 --dry-run # preview, writes nothing
76
+ azdo pr comments add --file plan.md --pr-number 64 # NEW thread on the overview
77
+ azdo pr comments edit 148 --file plan.md --pr-number 64 # rewrite it in place
67
78
  azdo pr comments reply 148 "Great suggestion, I'll address it." # human-readable output
68
79
  azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
69
80
  azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
70
81
 
82
+ # Any pr subcommand can target another repository
83
+ azdo pr comments --repo other-repo --pr-number 12
84
+
71
85
  # Pipelines — list, inspect runs, wait (exit code = result), start
72
86
  azdo pipeline list --filter ci
73
87
  azdo pipeline get-runs 12 --branch develop --limit 1
package/dist/index.js CHANGED
@@ -769,13 +769,29 @@ async function resolveAuthCredential(org) {
769
769
  }
770
770
  return null;
771
771
  }
772
+ var lastResolvedCredential = null;
772
773
  async function requireAuthCredential(org) {
773
774
  const cred = await resolveAuthCredential(org);
774
775
  if (cred !== null) {
776
+ lastResolvedCredential = { credential: cred, org };
775
777
  return cred;
776
778
  }
777
779
  throw new CredentialMissingError(org);
778
780
  }
781
+ function describeResolvedCredential() {
782
+ if (lastResolvedCredential === null) {
783
+ return null;
784
+ }
785
+ const { credential, org } = lastResolvedCredential;
786
+ if (credential.source === "env") {
787
+ return "Token used: PAT from the AZDO_PAT environment variable (it takes precedence over the stored credential). Fix: give that token the scope above, or unset AZDO_PAT to fall back to the stored credential.";
788
+ }
789
+ if (credential.source === "credential-store") {
790
+ const kind = credential.kind === "oauth" ? "OAuth access token" : "PAT";
791
+ return `Token used: ${kind} stored for org "${org}" in the OS credential store. Fix: run \`azdo auth login --org ${org}\` with an account or token carrying the scope above.`;
792
+ }
793
+ return "Token used: PAT entered at the prompt. Fix: re-enter a token carrying the scope above.";
794
+ }
779
795
  async function validatePatAgainstAzdo(pat, org) {
780
796
  const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?$top=1&api-version=7.1`;
781
797
  const auth = Buffer.from(`:${pat}`).toString("base64");
@@ -1781,6 +1797,27 @@ async function runConnectivityTest(org, cred) {
1781
1797
  }
1782
1798
  return { status: "failed", error };
1783
1799
  }
1800
+ async function resolveCredentialIdentity(org, cred) {
1801
+ const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/connectionData?api-version=7.1`;
1802
+ try {
1803
+ const result = await fetchRaw(url, { headers: authHeaders(cred) });
1804
+ if (result.status < 200 || result.status >= 300) {
1805
+ return null;
1806
+ }
1807
+ const parsed = JSON.parse(result.body);
1808
+ const user = parsed.authenticatedUser;
1809
+ if (user === void 0) {
1810
+ return null;
1811
+ }
1812
+ return {
1813
+ displayName: user.providerDisplayName ?? null,
1814
+ uniqueName: user.properties?.Account?.$value ?? null,
1815
+ id: user.id ?? null
1816
+ };
1817
+ } catch {
1818
+ return null;
1819
+ }
1820
+ }
1784
1821
  async function diagnoseAuth(org, project, resolveCredential) {
1785
1822
  const cred = await resolveCredential(org);
1786
1823
  if (cred === null) {
@@ -1790,19 +1827,22 @@ async function diagnoseAuth(org, project, resolveCredential) {
1790
1827
  org,
1791
1828
  project,
1792
1829
  connectivityStatus: "no-credentials",
1793
- connectivityError: null
1830
+ connectivityError: null,
1831
+ identity: null
1794
1832
  };
1795
1833
  }
1796
1834
  const connectivity = await runConnectivityTest(org, cred);
1797
1835
  const envVarName = process.env.AZDO_PAT ? "AZDO_PAT" : "dotenv";
1798
1836
  const sourceLabel = cred.source === "env" ? `env:${envVarName}` : "credential-store";
1837
+ const identity = connectivity.status === "ok" ? await resolveCredentialIdentity(org, cred) : null;
1799
1838
  return {
1800
1839
  authType: cred.kind ?? "pat",
1801
1840
  credentialSource: sourceLabel,
1802
1841
  org,
1803
1842
  project,
1804
1843
  connectivityStatus: connectivity.status,
1805
- connectivityError: connectivity.error
1844
+ connectivityError: connectivity.error,
1845
+ identity
1806
1846
  };
1807
1847
  }
1808
1848
  function formatDiagnosticReport(report, json) {
@@ -1824,6 +1864,13 @@ function formatDiagnosticReport(report, json) {
1824
1864
  `Project: ${report.project ?? "(not set)"}`,
1825
1865
  `Connectivity: ${connectivityLine}`
1826
1866
  ];
1867
+ const identity = report.identity;
1868
+ if (identity) {
1869
+ lines.push(`Identity: ${identity.displayName ?? "(unknown name)"} <${identity.uniqueName ?? "(unknown account)"}>`);
1870
+ if (identity.id) {
1871
+ lines.push(`Identity id: ${identity.id}`);
1872
+ }
1873
+ }
1827
1874
  if (report.connectivityStatus === "failed" && report.connectivityError !== null) {
1828
1875
  lines.push(`Error: ${report.connectivityError}`);
1829
1876
  }
@@ -2366,6 +2413,21 @@ async function promptForSetting(cfg, setting, ask) {
2366
2413
  function createConfigCommand() {
2367
2414
  const config = new Command4("config");
2368
2415
  config.description("Manage CLI settings");
2416
+ config.addHelpText(
2417
+ "after",
2418
+ [
2419
+ "",
2420
+ "Credentials are NOT stored in the configuration file. They are resolved in this order:",
2421
+ " 1. the AZDO_PAT environment variable (wins over everything below)",
2422
+ " 2. the OS credential store, per organization (see `azdo auth login`)",
2423
+ " 3. a .env file with AZDO_PAT, searched upwards from the working directory",
2424
+ "",
2425
+ "Only AZDO_PAT is read \u2014 AZURE_DEVOPS_PAT, AZURE_DEVOPS_EXT_PAT and AZDO_TOKEN are ignored.",
2426
+ "A pull request command needs a credential with the Code (Read) scope, or Code (Read & Write)",
2427
+ "to post, edit or resolve comments; Work Items scopes alone are not enough.",
2428
+ "Run `azdo auth diagnose` to see which credential is in use."
2429
+ ].join("\n")
2430
+ );
2369
2431
  const set = new Command4("set");
2370
2432
  set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
2371
2433
  try {
@@ -3158,6 +3220,7 @@ function createListFieldsCommand() {
3158
3220
  }
3159
3221
 
3160
3222
  // src/commands/pr.ts
3223
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3161
3224
  import { Command as Command12 } from "commander";
3162
3225
 
3163
3226
  // src/services/pr-client.ts
@@ -3166,13 +3229,18 @@ function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
3166
3229
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
3167
3230
  );
3168
3231
  url.searchParams.set("api-version", "7.1");
3169
- url.searchParams.set("searchCriteria.sourceRefName", `refs/heads/${sourceBranch}`);
3232
+ if (sourceBranch !== null) {
3233
+ url.searchParams.set("searchCriteria.sourceRefName", `refs/heads/${sourceBranch}`);
3234
+ }
3170
3235
  if (opts?.status) {
3171
3236
  url.searchParams.set("searchCriteria.status", opts.status);
3172
3237
  }
3173
3238
  if (opts?.targetBranch) {
3174
3239
  url.searchParams.set("searchCriteria.targetRefName", `refs/heads/${opts.targetBranch}`);
3175
3240
  }
3241
+ if (opts?.top !== void 0) {
3242
+ url.searchParams.set("$top", String(opts.top));
3243
+ }
3176
3244
  return url;
3177
3245
  }
3178
3246
  function buildPullRequestStatusesUrl(context, repo, prId) {
@@ -3207,7 +3275,10 @@ function buildPullRequestBuildsUrl(context, prId) {
3207
3275
  url.searchParams.set("api-version", "7.1");
3208
3276
  return url;
3209
3277
  }
3210
- function mapPullRequest(repo, pullRequest) {
3278
+ function buildPullRequestWebUrl(context, repo, prId) {
3279
+ return `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_git/${encodeURIComponent(repo)}/pullrequest/${prId}`;
3280
+ }
3281
+ function mapPullRequest(context, repo, pullRequest) {
3211
3282
  return {
3212
3283
  id: pullRequest.pullRequestId,
3213
3284
  title: pullRequest.title,
@@ -3216,7 +3287,10 @@ function mapPullRequest(repo, pullRequest) {
3216
3287
  targetRefName: pullRequest.targetRefName,
3217
3288
  status: pullRequest.status,
3218
3289
  createdBy: pullRequest.createdBy?.displayName ?? null,
3219
- url: pullRequest._links?.web?.href ?? null
3290
+ url: pullRequest._links?.web?.href ?? buildPullRequestWebUrl(context, repo, pullRequest.pullRequestId),
3291
+ description: pullRequest.description?.trim() || null,
3292
+ createdByUniqueName: pullRequest.createdBy?.uniqueName ?? null,
3293
+ createdById: pullRequest.createdBy?.id ?? null
3220
3294
  };
3221
3295
  }
3222
3296
  function mapPullRequestCheckName(status2) {
@@ -3316,7 +3390,8 @@ function mapComment(comment) {
3316
3390
  id: comment.id,
3317
3391
  author: comment.author?.displayName ?? null,
3318
3392
  content,
3319
- publishedAt: comment.publishedDate ?? null
3393
+ publishedAt: comment.publishedDate ?? null,
3394
+ commentType: comment.commentType ?? null
3320
3395
  };
3321
3396
  }
3322
3397
  function mapThread(thread) {
@@ -3376,7 +3451,7 @@ async function getPullRequestById(context, repo, cred, prId) {
3376
3451
  url.searchParams.set("api-version", "7.1");
3377
3452
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3378
3453
  const data = await readJsonResponse(response);
3379
- return mapPullRequest(repo, data);
3454
+ return mapPullRequest(context, repo, data);
3380
3455
  }
3381
3456
  async function listPullRequests(context, repo, cred, sourceBranch, opts) {
3382
3457
  const response = await fetchWithErrors(
@@ -3384,7 +3459,16 @@ async function listPullRequests(context, repo, cred, sourceBranch, opts) {
3384
3459
  { headers: authHeaders(cred) }
3385
3460
  );
3386
3461
  const data = await readJsonResponse(response);
3387
- return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
3462
+ return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
3463
+ }
3464
+ async function listRepositoryPullRequests(context, repo, cred, opts) {
3465
+ const url = buildPullRequestsUrl(context, repo, opts?.sourceBranch ?? null, {
3466
+ status: opts?.status,
3467
+ top: opts?.top
3468
+ });
3469
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3470
+ const data = await readJsonResponse(response);
3471
+ return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
3388
3472
  }
3389
3473
  async function getPullRequestChecks(context, repo, cred, prId) {
3390
3474
  const response = await fetchWithErrors(
@@ -3466,7 +3550,7 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
3466
3550
  branch: sourceBranch,
3467
3551
  targetBranch: "develop",
3468
3552
  created: true,
3469
- pullRequest: mapPullRequest(repo, data)
3553
+ pullRequest: mapPullRequest(context, repo, data)
3470
3554
  };
3471
3555
  }
3472
3556
  async function getPullRequestThreads(context, repo, cred, prId) {
@@ -3478,6 +3562,37 @@ async function getPullRequestThreads(context, repo, cred, prId) {
3478
3562
  const data = await readJsonResponse(response);
3479
3563
  return data.value.map(mapThread).filter((thread) => thread !== null);
3480
3564
  }
3565
+ async function getPullRequestThread(context, repo, cred, prId, threadId) {
3566
+ const url = new URL(
3567
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}`
3568
+ );
3569
+ url.searchParams.set("api-version", "7.1");
3570
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3571
+ const data = await readJsonResponse(response);
3572
+ return toActiveCommentThread(data);
3573
+ }
3574
+ async function createPullRequestThread(context, repo, cred, prId, content, status2) {
3575
+ const url = new URL(
3576
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
3577
+ );
3578
+ url.searchParams.set("api-version", "7.1");
3579
+ const payload = {
3580
+ comments: [{ parentCommentId: 0, content, commentType: 1 }]
3581
+ };
3582
+ if (status2 !== void 0) {
3583
+ payload.status = status2;
3584
+ }
3585
+ const response = await fetchWithErrors(url.toString(), {
3586
+ method: "POST",
3587
+ headers: {
3588
+ ...authHeaders(cred),
3589
+ "Content-Type": "application/json"
3590
+ },
3591
+ body: JSON.stringify(payload)
3592
+ });
3593
+ const data = await readJsonResponse(response);
3594
+ return toActiveCommentThread(data);
3595
+ }
3481
3596
  function buildThreadCommentUrl(context, repo, prId, threadId) {
3482
3597
  const url = new URL(
3483
3598
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments`
@@ -3485,6 +3600,27 @@ function buildThreadCommentUrl(context, repo, prId, threadId) {
3485
3600
  url.searchParams.set("api-version", "7.1");
3486
3601
  return url;
3487
3602
  }
3603
+ async function updateThreadComment(context, repo, cred, prId, threadId, commentId, content) {
3604
+ const url = new URL(
3605
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments/${commentId}`
3606
+ );
3607
+ url.searchParams.set("api-version", "7.1");
3608
+ const response = await fetchWithErrors(url.toString(), {
3609
+ method: "PATCH",
3610
+ headers: {
3611
+ ...authHeaders(cred),
3612
+ "Content-Type": "application/json"
3613
+ },
3614
+ body: JSON.stringify({ content })
3615
+ });
3616
+ const data = await readJsonResponse(response);
3617
+ return {
3618
+ id: data.id,
3619
+ author: data.author?.displayName ?? null,
3620
+ content: data.content ?? content,
3621
+ publishedAt: data.publishedDate ?? null
3622
+ };
3623
+ }
3488
3624
  async function postThreadComment(context, repo, cred, prId, threadId, content) {
3489
3625
  const response = await fetchWithErrors(buildThreadCommentUrl(context, repo, prId, threadId).toString(), {
3490
3626
  method: "POST",
@@ -3512,9 +3648,16 @@ function parsePositivePrNumber(raw) {
3512
3648
  return Number.isFinite(n) && n > 0 ? n : null;
3513
3649
  }
3514
3650
  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.";
3651
+ var PR_REPO_HELP = 'Azure DevOps repository name; defaults to the repository of the git "origin" remote';
3515
3652
  function configureUnwrappedHelp(command) {
3516
3653
  return command.configureHelp({ helpWidth: 1e3 });
3517
3654
  }
3655
+ function withCommonPrOptions(command) {
3656
+ return command.option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--repo <name>", PR_REPO_HELP);
3657
+ }
3658
+ function mergedPrOptions(command) {
3659
+ return command.optsWithGlobals();
3660
+ }
3518
3661
  function autoDetectZeroMatch(branch) {
3519
3662
  return `No open pull request matches branch ${branch}. Pass --pr-number to target a specific PR, or push the branch and open a pull request.`;
3520
3663
  }
@@ -3527,23 +3670,74 @@ function writeContractError(line) {
3527
3670
  `);
3528
3671
  process.exitCode = 1;
3529
3672
  }
3673
+ var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
3674
+ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
3675
+ if (inline !== void 0 && file !== void 0) {
3676
+ writeError("Cannot specify both inline text and --file.");
3677
+ return null;
3678
+ }
3679
+ let body;
3680
+ if (file !== void 0) {
3681
+ if (!existsSync5(file)) {
3682
+ writeError(`File not found: ${file}`);
3683
+ return null;
3684
+ }
3685
+ try {
3686
+ body = readFileSync5(file, "utf-8");
3687
+ } catch {
3688
+ writeError(`Cannot read file: ${file}`);
3689
+ return null;
3690
+ }
3691
+ } else if (inline !== void 0) {
3692
+ body = inline;
3693
+ } else {
3694
+ writeError(emptyMessage);
3695
+ return null;
3696
+ }
3697
+ const trimmed = body.trim();
3698
+ if (trimmed === "") {
3699
+ writeError(emptyMessage);
3700
+ return null;
3701
+ }
3702
+ return trimmed;
3703
+ }
3704
+ function parseNonNegativeInt(raw) {
3705
+ if (!/^\d+$/.test(raw)) {
3706
+ return null;
3707
+ }
3708
+ const n = Number.parseInt(raw, 10);
3709
+ return Number.isFinite(n) ? n : null;
3710
+ }
3711
+ function truncateContent(text, limit) {
3712
+ if (limit <= 0 || text.length <= limit) {
3713
+ return { content: text, truncated: false, originalLength: text.length };
3714
+ }
3715
+ return { content: `${text.slice(0, limit)} [\u2026]`, truncated: true, originalLength: text.length };
3716
+ }
3530
3717
  function formatBranchName(refName) {
3531
3718
  return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
3532
3719
  }
3533
- function writeError(message) {
3720
+ var EXIT_NOT_FOUND = 3;
3721
+ var EXIT_NOT_PERMITTED = 4;
3722
+ function writeError(message, exitCode = 1) {
3534
3723
  process.stderr.write(`Error: ${message}
3535
3724
  `);
3536
- process.exitCode = 1;
3725
+ process.exitCode = exitCode;
3537
3726
  }
3538
3727
  function handlePrCommandError(err, context, mode = "read") {
3539
3728
  const error = err instanceof Error ? err : new Error(String(err));
3540
3729
  if (error.message === "AUTH_FAILED") {
3541
3730
  const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
3542
- writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`);
3731
+ writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`, EXIT_NOT_PERMITTED);
3732
+ const credentialHint = describeResolvedCredential();
3733
+ if (credentialHint !== null) {
3734
+ process.stderr.write(` ${credentialHint}
3735
+ `);
3736
+ }
3543
3737
  return;
3544
3738
  }
3545
3739
  if (error.message === "PERMISSION_DENIED") {
3546
- writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`);
3740
+ writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
3547
3741
  return;
3548
3742
  }
3549
3743
  if (error.message === "NETWORK_ERROR") {
@@ -3551,7 +3745,7 @@ function handlePrCommandError(err, context, mode = "read") {
3551
3745
  return;
3552
3746
  }
3553
3747
  if (error.message.startsWith("NOT_FOUND")) {
3554
- writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
3748
+ writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`, EXIT_NOT_FOUND);
3555
3749
  return;
3556
3750
  }
3557
3751
  if (error.message.startsWith("HTTP_")) {
@@ -3649,6 +3843,22 @@ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, pro
3649
3843
  function threadStatusLabel(status2) {
3650
3844
  return isThreadResolved(status2) ? "resolved" : status2;
3651
3845
  }
3846
+ function shapeThreadForOutput(thread, opts) {
3847
+ const kept = thread.comments.filter(
3848
+ (comment) => !opts.excludeSystem || comment.commentType !== "system"
3849
+ );
3850
+ if (opts.excludeSystem && kept.length === 0) {
3851
+ return null;
3852
+ }
3853
+ if (opts.contains !== void 0 && !kept.some((comment) => comment.content.includes(opts.contains))) {
3854
+ return null;
3855
+ }
3856
+ const comments = kept.map((comment) => ({
3857
+ ...comment,
3858
+ ...truncateContent(comment.content, opts.maxChars)
3859
+ }));
3860
+ return { ...thread, comments };
3861
+ }
3652
3862
  function formatThreads(prId, title, threads) {
3653
3863
  const lines = [`Comment threads for pull request #${prId}: ${title}`];
3654
3864
  for (const thread of threads) {
@@ -3664,7 +3874,7 @@ function formatThreads(prId, title, threads) {
3664
3874
  async function resolvePrCommandContext(options, resolveOpts = {}) {
3665
3875
  const requireBranch = resolveOpts.requireBranch ?? true;
3666
3876
  const context = resolveContext(options);
3667
- const repo = detectRepoName();
3877
+ const repo = options.repo?.trim() || detectRepoName();
3668
3878
  const branch = requireBranch ? getCurrentBranch() : null;
3669
3879
  const credential = await requireAuthCredential(context.org);
3670
3880
  return {
@@ -3676,7 +3886,7 @@ async function resolvePrCommandContext(options, resolveOpts = {}) {
3676
3886
  }
3677
3887
  function createPrStatusCommand() {
3678
3888
  const command = new Command12("status");
3679
- command.description("Check pull requests for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
3889
+ withCommonPrOptions(command).description("Check pull requests for the current branch").option("--json", "output JSON").action(async (options) => {
3680
3890
  validateOrgProjectPair(options);
3681
3891
  let context;
3682
3892
  try {
@@ -3716,7 +3926,7 @@ function createPrStatusCommand() {
3716
3926
  }
3717
3927
  function createPrOpenCommand() {
3718
3928
  const command = new Command12("open");
3719
- command.description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option("--description <description>", "pull request description").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
3929
+ withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option("--description <description>", "pull request description").option("--json", "output JSON").action(async (options) => {
3720
3930
  validateOrgProjectPair(options);
3721
3931
  const title = options.title?.trim();
3722
3932
  if (!title) {
@@ -3774,7 +3984,7 @@ ${result.pullRequest.url ?? "\u2014"}
3774
3984
  }
3775
3985
  function createPrCommentsCommand() {
3776
3986
  const command = new Command12("comments");
3777
- configureUnwrappedHelp(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>", 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("--json", "output JSON").action(async (options) => {
3987
+ 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) => {
3778
3988
  validateOrgProjectPair(options);
3779
3989
  let context;
3780
3990
  let explicitPrId = null;
@@ -3785,6 +3995,23 @@ function createPrCommentsCommand() {
3785
3995
  return;
3786
3996
  }
3787
3997
  }
3998
+ let maxChars = 0;
3999
+ if (options.maxChars !== void 0) {
4000
+ const parsed = parseNonNegativeInt(options.maxChars);
4001
+ if (parsed === null) {
4002
+ writeError(`Invalid --max-chars "${options.maxChars}"; expected a non-negative integer.`);
4003
+ return;
4004
+ }
4005
+ maxChars = parsed;
4006
+ }
4007
+ let threadFilter = null;
4008
+ if (options.thread !== void 0) {
4009
+ threadFilter = parsePositivePrNumber(options.thread);
4010
+ if (threadFilter === null) {
4011
+ writeError(`Invalid --thread "${options.thread}"; expected a positive integer.`);
4012
+ return;
4013
+ }
4014
+ }
3788
4015
  try {
3789
4016
  const resolved = await resolvePrCommandContext(options, { requireBranch: explicitPrId === null });
3790
4017
  context = resolved.context;
@@ -3795,7 +4022,7 @@ function createPrCommentsCommand() {
3795
4022
  pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
3796
4023
  } catch (err) {
3797
4024
  if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
3798
- writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
4025
+ writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`, EXIT_NOT_FOUND);
3799
4026
  return;
3800
4027
  }
3801
4028
  throw err;
@@ -3818,10 +4045,16 @@ function createPrCommentsCommand() {
3818
4045
  }
3819
4046
  const hideResolved = options.hideResolved === true || options.excludeResolved === true;
3820
4047
  const codeRelatedOnly = options.codeRelatedOnly === true;
3821
- const allThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
4048
+ const excludeSystem = options.excludeSystem === true;
4049
+ const fetchedThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
4050
+ if (threadFilter !== null && !fetchedThreads.some((thread) => thread.id === threadFilter)) {
4051
+ writeError(`Thread #${threadFilter} not found on pull request #${pullRequest.id}.`, EXIT_NOT_FOUND);
4052
+ return;
4053
+ }
4054
+ const allThreads = threadFilter === null ? fetchedThreads : fetchedThreads.filter((thread) => thread.id === threadFilter);
3822
4055
  const threads = allThreads.filter(
3823
4056
  (thread) => (!hideResolved || !isThreadResolved(thread.status)) && (!codeRelatedOnly || thread.threadContext !== null)
3824
- );
4057
+ ).map((thread) => shapeThreadForOutput(thread, { excludeSystem, maxChars, contains: options.contains })).filter((thread) => thread !== null);
3825
4058
  const result = { branch: branchLabel, pullRequest, threads };
3826
4059
  if (options.json) {
3827
4060
  process.stdout.write(`${JSON.stringify(result, null, 2)}
@@ -3829,14 +4062,20 @@ function createPrCommentsCommand() {
3829
4062
  return;
3830
4063
  }
3831
4064
  if (threads.length === 0) {
3832
- if (allThreads.length > 0 && (hideResolved || codeRelatedOnly)) {
4065
+ if (allThreads.length > 0 && (hideResolved || codeRelatedOnly || excludeSystem || options.contains !== void 0)) {
3833
4066
  const filters = [];
4067
+ if (options.contains !== void 0) {
4068
+ filters.push("matching");
4069
+ }
3834
4070
  if (codeRelatedOnly) {
3835
4071
  filters.push("code-related");
3836
4072
  }
3837
4073
  if (hideResolved) {
3838
4074
  filters.push("unresolved");
3839
4075
  }
4076
+ if (excludeSystem) {
4077
+ filters.push("non-system");
4078
+ }
3840
4079
  process.stdout.write(
3841
4080
  `Pull request #${pullRequest.id} has no ${filters.join(" ")} comment threads (filtered from ${allThreads.length} thread${allThreads.length === 1 ? "" : "s"}).
3842
4081
  `
@@ -3854,15 +4093,12 @@ function createPrCommentsCommand() {
3854
4093
  }
3855
4094
  });
3856
4095
  command.addCommand(createPrCommentsReplyCommand());
4096
+ command.addCommand(createPrCommentsAddCommand());
4097
+ command.addCommand(createPrCommentsEditCommand());
3857
4098
  return command;
3858
4099
  }
3859
- async function resolveThreadTarget(threadIdRaw, options) {
4100
+ async function resolvePullRequestTarget(options) {
3860
4101
  validateOrgProjectPair(options);
3861
- const threadId = parsePositivePrNumber(threadIdRaw);
3862
- if (threadId === null) {
3863
- writeError(`Invalid thread id "${threadIdRaw}"; expected a positive integer.`);
3864
- return null;
3865
- }
3866
4102
  let explicitPrId = null;
3867
4103
  if (options.prNumber !== void 0) {
3868
4104
  explicitPrId = parsePositivePrNumber(options.prNumber);
@@ -3878,7 +4114,7 @@ async function resolveThreadTarget(threadIdRaw, options) {
3878
4114
  pullRequest = await getPullRequestById(resolved.context, resolved.repo, resolved.pat, explicitPrId);
3879
4115
  } catch (err) {
3880
4116
  if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
3881
- writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`);
4117
+ writeError(`Pull request #${explicitPrId} not found in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`, EXIT_NOT_FOUND);
3882
4118
  return null;
3883
4119
  }
3884
4120
  throw err;
@@ -3897,7 +4133,20 @@ async function resolveThreadTarget(threadIdRaw, options) {
3897
4133
  }
3898
4134
  pullRequest = pullRequests[0];
3899
4135
  }
3900
- return { context: resolved.context, repo: resolved.repo, pat: resolved.pat, pullRequest, threadId };
4136
+ return { context: resolved.context, repo: resolved.repo, pat: resolved.pat, pullRequest };
4137
+ }
4138
+ async function resolveThreadTarget(threadIdRaw, options) {
4139
+ const threadId = parsePositivePrNumber(threadIdRaw);
4140
+ if (threadId === null) {
4141
+ validateOrgProjectPair(options);
4142
+ writeError(`Invalid thread id "${threadIdRaw}"; expected a positive integer.`);
4143
+ return null;
4144
+ }
4145
+ const target = await resolvePullRequestTarget(options);
4146
+ if (target === null) {
4147
+ return null;
4148
+ }
4149
+ return { ...target, threadId };
3901
4150
  }
3902
4151
  async function runThreadStateChange(threadIdRaw, options, direction) {
3903
4152
  let context;
@@ -3910,7 +4159,7 @@ async function runThreadStateChange(threadIdRaw, options, direction) {
3910
4159
  const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
3911
4160
  const thread = threads.find((t) => t.id === target.threadId);
3912
4161
  if (!thread) {
3913
- writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`);
4162
+ writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
3914
4163
  return;
3915
4164
  }
3916
4165
  const alreadyInTargetState = direction === "resolve" ? isThreadResolved(thread.status) : !isThreadResolved(thread.status);
@@ -3960,14 +4209,14 @@ async function runThreadStateChange(threadIdRaw, options, direction) {
3960
4209
  }
3961
4210
  function createPrCommentResolveCommand() {
3962
4211
  const command = new Command12("comment-resolve");
3963
- configureUnwrappedHelp(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>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
4212
+ withCommonPrOptions(configureUnwrappedHelp(command)).description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
3964
4213
  await runThreadStateChange(threadIdRaw, options, "resolve");
3965
4214
  });
3966
4215
  return command;
3967
4216
  }
3968
4217
  function createPrCommentReopenCommand() {
3969
4218
  const command = new Command12("comment-reopen");
3970
- configureUnwrappedHelp(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>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
4219
+ withCommonPrOptions(configureUnwrappedHelp(command)).description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
3971
4220
  await runThreadStateChange(threadIdRaw, options, "reopen");
3972
4221
  });
3973
4222
  return command;
@@ -3975,9 +4224,8 @@ function createPrCommentReopenCommand() {
3975
4224
  async function runCommentReply(threadIdRaw, text, options) {
3976
4225
  let context;
3977
4226
  try {
3978
- const trimmedText = text.trim();
3979
- if (!trimmedText) {
3980
- writeError("Reply text must not be empty.");
4227
+ const trimmedText = resolveCommentBody(text, options.file, "Reply text must not be empty.");
4228
+ if (trimmedText === null) {
3981
4229
  return;
3982
4230
  }
3983
4231
  const target = await resolveThreadTarget(threadIdRaw, options);
@@ -3988,7 +4236,7 @@ async function runCommentReply(threadIdRaw, text, options) {
3988
4236
  const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
3989
4237
  const thread = threads.find((t) => t.id === target.threadId);
3990
4238
  if (!thread) {
3991
- writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`);
4239
+ writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
3992
4240
  return;
3993
4241
  }
3994
4242
  const posted = await postThreadComment(
@@ -4016,29 +4264,333 @@ async function runCommentReply(threadIdRaw, text, options) {
4016
4264
  handlePrCommandError(err, context, "write");
4017
4265
  }
4018
4266
  }
4019
- function createPrCommentsReplyCommand() {
4020
- const command = new Command12("reply");
4021
- configureUnwrappedHelp(command).description("Post a reply to a pull request comment thread").argument("<threadId>", "numeric id of the thread to reply to").argument("<text>", "text of the reply").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, options) => {
4022
- await runCommentReply(threadIdRaw, text, options);
4267
+ function buildCommentReplyCommand(name, description) {
4268
+ const command = new Command12(name);
4269
+ withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread to reply to").argument("[text]", "text of the reply; omit when using --file").option("--file <path>", "read the reply body from a UTF-8 file instead of the inline argument").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, _options, command2) => {
4270
+ await runCommentReply(threadIdRaw, text, mergedPrOptions(command2));
4023
4271
  });
4024
4272
  return command;
4025
4273
  }
4274
+ function createPrCommentsReplyCommand() {
4275
+ return buildCommentReplyCommand("reply", "Post a reply to a pull request comment thread");
4276
+ }
4026
4277
  function createPrCommentReplyCommand() {
4027
- const command = new Command12("comment-reply");
4028
- configureUnwrappedHelp(command).description('Post a reply to a pull request comment thread (alias of "azdo pr comments reply")').argument("<threadId>", "numeric id of the thread to reply to").argument("<text>", "text of the reply").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, options) => {
4029
- await runCommentReply(threadIdRaw, text, options);
4278
+ return buildCommentReplyCommand(
4279
+ "comment-reply",
4280
+ 'Post a reply to a pull request comment thread (alias of "azdo pr comments reply")'
4281
+ );
4282
+ }
4283
+ var CREATABLE_THREAD_STATUSES = [
4284
+ "active",
4285
+ "fixed",
4286
+ "wontFix",
4287
+ "closed",
4288
+ "byDesign",
4289
+ "pending"
4290
+ ];
4291
+ async function runCommentAdd(text, options) {
4292
+ let context;
4293
+ try {
4294
+ const body = resolveCommentBody(text, options.file);
4295
+ if (body === null) {
4296
+ return;
4297
+ }
4298
+ let status2;
4299
+ if (options.status !== void 0) {
4300
+ const match = CREATABLE_THREAD_STATUSES.find((candidate) => candidate === options.status);
4301
+ if (match === void 0) {
4302
+ writeError(
4303
+ `Invalid --status "${options.status}"; expected one of ${CREATABLE_THREAD_STATUSES.join(", ")}.`
4304
+ );
4305
+ return;
4306
+ }
4307
+ status2 = match;
4308
+ }
4309
+ const target = await resolvePullRequestTarget(options);
4310
+ if (target === null) {
4311
+ return;
4312
+ }
4313
+ context = target.context;
4314
+ if (options.dryRun === true) {
4315
+ const dryResult = {
4316
+ pullRequestId: target.pullRequest.id,
4317
+ threadId: null,
4318
+ commentId: null,
4319
+ status: status2 ?? null,
4320
+ content: body,
4321
+ dryRun: true
4322
+ };
4323
+ if (options.json) {
4324
+ process.stdout.write(`${JSON.stringify(dryResult, null, 2)}
4325
+ `);
4326
+ return;
4327
+ }
4328
+ const statusSuffix = status2 === void 0 ? "" : ` with status ${status2}`;
4329
+ process.stdout.write(
4330
+ `Dry run: would post a new comment thread${statusSuffix} on pull request #${target.pullRequest.id} (${body.length} chars).
4331
+ ${body}
4332
+ `
4333
+ );
4334
+ return;
4335
+ }
4336
+ const thread = await createPullRequestThread(
4337
+ target.context,
4338
+ target.repo,
4339
+ target.pat,
4340
+ target.pullRequest.id,
4341
+ body,
4342
+ status2
4343
+ );
4344
+ const created = thread.comments[0];
4345
+ const result = {
4346
+ pullRequestId: target.pullRequest.id,
4347
+ threadId: thread.id,
4348
+ commentId: created?.id ?? null,
4349
+ status: thread.status,
4350
+ content: created?.content ?? body,
4351
+ dryRun: false
4352
+ };
4353
+ if (options.json) {
4354
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4355
+ `);
4356
+ return;
4357
+ }
4358
+ process.stdout.write(
4359
+ `Comment posted to pull request #${target.pullRequest.id} (thread #${thread.id}).
4360
+ `
4361
+ );
4362
+ } catch (err) {
4363
+ handlePrCommandError(err, context, "write");
4364
+ }
4365
+ }
4366
+ function buildCommentAddCommand(name, description) {
4367
+ const command = new Command12(name);
4368
+ withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("[text]", "body of the new comment; omit when using --file").option("--file <path>", "read the comment body from a UTF-8 file instead of the inline argument").option(
4369
+ "--status <status>",
4370
+ `thread status (${CREATABLE_THREAD_STATUSES.join(" | ")}); omit for a plain, non-resolvable comment`
4371
+ ).option("--dry-run", "resolve the target pull request and print what would be posted, without writing anything").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (text, _options, command2) => {
4372
+ await runCommentAdd(text, mergedPrOptions(command2));
4373
+ });
4374
+ return command;
4375
+ }
4376
+ function createPrCommentsAddCommand() {
4377
+ return buildCommentAddCommand("add", "Post a new comment thread on the pull request overview");
4378
+ }
4379
+ function createPrCommentAddCommand() {
4380
+ return buildCommentAddCommand(
4381
+ "comment-add",
4382
+ 'Post a new comment thread on the pull request overview (alias of "azdo pr comments add")'
4383
+ );
4384
+ }
4385
+ async function fetchThreadForEdit(target) {
4386
+ try {
4387
+ return await getPullRequestThread(
4388
+ target.context,
4389
+ target.repo,
4390
+ target.pat,
4391
+ target.pullRequest.id,
4392
+ target.threadId
4393
+ );
4394
+ } catch (err) {
4395
+ if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
4396
+ writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
4397
+ return null;
4398
+ }
4399
+ throw err;
4400
+ }
4401
+ }
4402
+ function selectEditableComment(thread, explicitCommentId, target) {
4403
+ if (explicitCommentId !== null) {
4404
+ const match = thread.comments.find((comment) => comment.id === explicitCommentId);
4405
+ if (match === void 0) {
4406
+ writeError(`Comment #${explicitCommentId} not found in thread #${target.threadId} on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
4407
+ return null;
4408
+ }
4409
+ return match;
4410
+ }
4411
+ const first = [...thread.comments].sort((a, b) => a.id - b.id)[0];
4412
+ if (first === void 0) {
4413
+ writeError(`Thread #${target.threadId} on pull request #${target.pullRequest.id} has no editable comment.`, EXIT_NOT_FOUND);
4414
+ return null;
4415
+ }
4416
+ return first;
4417
+ }
4418
+ function reportEditResult(result, json) {
4419
+ if (json) {
4420
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4421
+ `);
4422
+ return;
4423
+ }
4424
+ if (result.dryRun) {
4425
+ process.stdout.write(
4426
+ `Dry run: would replace comment #${result.commentId} in thread #${result.threadId} on pull request #${result.pullRequestId} (${result.previousContent.length} chars -> ${result.content.length} chars).
4427
+ ${result.content}
4428
+ `
4429
+ );
4430
+ return;
4431
+ }
4432
+ process.stdout.write(
4433
+ `Comment #${result.commentId} updated in thread #${result.threadId} on pull request #${result.pullRequestId}.
4434
+ `
4435
+ );
4436
+ }
4437
+ async function runCommentEdit(threadIdRaw, text, options) {
4438
+ let context;
4439
+ try {
4440
+ const body = resolveCommentBody(text, options.file);
4441
+ if (body === null) {
4442
+ return;
4443
+ }
4444
+ let explicitCommentId = null;
4445
+ if (options.commentId !== void 0) {
4446
+ explicitCommentId = parsePositivePrNumber(options.commentId);
4447
+ if (explicitCommentId === null) {
4448
+ writeError(`Invalid --comment-id "${options.commentId}"; expected a positive integer.`);
4449
+ return;
4450
+ }
4451
+ }
4452
+ const target = await resolveThreadTarget(threadIdRaw, options);
4453
+ if (target === null) {
4454
+ return;
4455
+ }
4456
+ context = target.context;
4457
+ const thread = await fetchThreadForEdit(target);
4458
+ if (thread === null) {
4459
+ return;
4460
+ }
4461
+ const existing = selectEditableComment(thread, explicitCommentId, target);
4462
+ if (existing === null) {
4463
+ return;
4464
+ }
4465
+ if (options.dryRun === true) {
4466
+ reportEditResult(
4467
+ {
4468
+ pullRequestId: target.pullRequest.id,
4469
+ threadId: target.threadId,
4470
+ commentId: existing.id,
4471
+ previousContent: existing.content,
4472
+ content: body,
4473
+ dryRun: true
4474
+ },
4475
+ options.json === true
4476
+ );
4477
+ return;
4478
+ }
4479
+ const updated = await updateThreadComment(
4480
+ target.context,
4481
+ target.repo,
4482
+ target.pat,
4483
+ target.pullRequest.id,
4484
+ target.threadId,
4485
+ existing.id,
4486
+ body
4487
+ );
4488
+ reportEditResult(
4489
+ {
4490
+ pullRequestId: target.pullRequest.id,
4491
+ threadId: target.threadId,
4492
+ commentId: updated.id,
4493
+ previousContent: existing.content,
4494
+ content: updated.content,
4495
+ dryRun: false
4496
+ },
4497
+ options.json === true
4498
+ );
4499
+ } catch (err) {
4500
+ handlePrCommandError(err, context, "write");
4501
+ }
4502
+ }
4503
+ function buildCommentEditCommand(name, description) {
4504
+ const command = new Command12(name);
4505
+ withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread holding the comment").argument("[text]", "new comment body; omit when using --file").option("--comment-id <N>", "numeric id of the comment to edit; defaults to the thread's first comment").option("--file <path>", "read the new body from a UTF-8 file instead of the inline argument").option("--dry-run", "print the replacement body plus the current/new lengths, without writing anything (--json also returns previousContent)").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, _options, command2) => {
4506
+ await runCommentEdit(threadIdRaw, text, mergedPrOptions(command2));
4507
+ });
4508
+ return command;
4509
+ }
4510
+ function createPrCommentsEditCommand() {
4511
+ return buildCommentEditCommand("edit", "Edit an existing pull request comment in place");
4512
+ }
4513
+ function createPrCommentEditCommand() {
4514
+ return buildCommentEditCommand(
4515
+ "comment-edit",
4516
+ 'Edit an existing pull request comment in place (alias of "azdo pr comments edit")'
4517
+ );
4518
+ }
4519
+ var LIST_STATUS_VALUES = ["active", "completed", "abandoned", "all"];
4520
+ var DEFAULT_LIST_TOP = 25;
4521
+ function formatPullRequestListEntry(pullRequest) {
4522
+ return [
4523
+ `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
4524
+ ` ${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
4525
+ ` Author: ${pullRequest.createdBy ?? "Unknown"}`,
4526
+ ` ${pullRequest.url ?? "\u2014"}`
4527
+ ].join("\n");
4528
+ }
4529
+ function createPrListCommand() {
4530
+ const command = new Command12("list");
4531
+ withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull requests in the repository, optionally filtered by source branch").option("--branch <name>", "only pull requests whose source branch is this one (with or without the refs/heads/ prefix)").option("--status <status>", `pull request status filter (${LIST_STATUS_VALUES.join(" | ")})`, "active").option("--top <N>", `maximum number of pull requests to return (default ${DEFAULT_LIST_TOP})`).option("--json", "output JSON").action(async (options) => {
4532
+ validateOrgProjectPair(options);
4533
+ const status2 = options.status ?? "active";
4534
+ if (!LIST_STATUS_VALUES.includes(status2)) {
4535
+ writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
4536
+ return;
4537
+ }
4538
+ let top = DEFAULT_LIST_TOP;
4539
+ if (options.top !== void 0) {
4540
+ const parsed = parsePositivePrNumber(options.top);
4541
+ if (parsed === null) {
4542
+ writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
4543
+ return;
4544
+ }
4545
+ top = parsed;
4546
+ }
4547
+ const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
4548
+ let context;
4549
+ try {
4550
+ const resolved = await resolvePrCommandContext(options, { requireBranch: false });
4551
+ context = resolved.context;
4552
+ const pullRequests = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
4553
+ sourceBranch: branch ?? void 0,
4554
+ status: status2,
4555
+ top
4556
+ });
4557
+ const result = {
4558
+ repository: resolved.repo,
4559
+ branch,
4560
+ status: status2,
4561
+ pullRequests
4562
+ };
4563
+ if (options.json) {
4564
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4565
+ `);
4566
+ return;
4567
+ }
4568
+ if (pullRequests.length === 0) {
4569
+ const branchSuffix = branch === null ? "" : ` for branch ${branch}`;
4570
+ process.stdout.write(`No ${status2} pull request found in ${resolved.repo}${branchSuffix}.
4571
+ `);
4572
+ return;
4573
+ }
4574
+ process.stdout.write(`${pullRequests.map(formatPullRequestListEntry).join("\n\n")}
4575
+ `);
4576
+ } catch (err) {
4577
+ handlePrCommandError(err, context, "read");
4578
+ }
4030
4579
  });
4031
4580
  return command;
4032
4581
  }
4033
4582
  function createPrCommand() {
4034
4583
  const command = new Command12("pr");
4035
4584
  command.description("Manage Azure DevOps pull requests");
4585
+ command.addCommand(createPrListCommand());
4036
4586
  command.addCommand(createPrStatusCommand());
4037
4587
  command.addCommand(createPrOpenCommand());
4038
4588
  command.addCommand(createPrCommentsCommand());
4039
4589
  command.addCommand(createPrCommentResolveCommand());
4040
4590
  command.addCommand(createPrCommentReopenCommand());
4041
4591
  command.addCommand(createPrCommentReplyCommand());
4592
+ command.addCommand(createPrCommentAddCommand());
4593
+ command.addCommand(createPrCommentEditCommand());
4042
4594
  return command;
4043
4595
  }
4044
4596
 
@@ -5040,7 +5592,7 @@ function createCommentsCommand() {
5040
5592
  // src/commands/download-attachment.ts
5041
5593
  import { Command as Command15 } from "commander";
5042
5594
  import { writeFile as writeFile2 } from "fs/promises";
5043
- import { existsSync as existsSync5 } from "fs";
5595
+ import { existsSync as existsSync6 } from "fs";
5044
5596
  import { join as join3 } from "path";
5045
5597
  function createDownloadAttachmentCommand() {
5046
5598
  const command = new Command15("download-attachment");
@@ -5053,7 +5605,7 @@ function createDownloadAttachmentCommand() {
5053
5605
  context = resolveContext(options);
5054
5606
  const credential = await requireAuthCredential(context.org);
5055
5607
  const outputDir = options.output ?? ".";
5056
- if (!existsSync5(outputDir)) {
5608
+ if (!existsSync6(outputDir)) {
5057
5609
  process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
5058
5610
  `);
5059
5611
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.15.0-develop.593",
3
+ "version": "0.15.0-develop.604",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {