azdo-cli 0.15.0-develop.594 → 0.15.0
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.
- package/README.md +15 -1
- package/dist/index.js +598 -46
- 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
|
-
#
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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 =
|
|
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("--
|
|
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("--
|
|
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("--
|
|
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
|
|
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
|
|
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
|
|
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("--
|
|
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("--
|
|
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.
|
|
3979
|
-
if (
|
|
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
|
|
4020
|
-
const command = new Command12(
|
|
4021
|
-
configureUnwrappedHelp(command).description(
|
|
4022
|
-
await runCommentReply(threadIdRaw, text,
|
|
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
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
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
|
|
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 (!
|
|
5608
|
+
if (!existsSync6(outputDir)) {
|
|
5057
5609
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
5058
5610
|
`);
|
|
5059
5611
|
process.exit(1);
|