azdo-cli 0.21.0 → 0.22.0-ci-harden-workflow.876
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/dist/index.js
CHANGED
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
tokenResponseToCredential,
|
|
40
40
|
unsetConfigValue,
|
|
41
41
|
unsetOrgScopedValue
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-MHJ6OBX3.js";
|
|
43
43
|
|
|
44
44
|
// src/program.ts
|
|
45
|
-
import { Command as
|
|
45
|
+
import { Command as Command20 } from "commander";
|
|
46
46
|
|
|
47
47
|
// src/version.ts
|
|
48
48
|
import { readFileSync } from "fs";
|
|
@@ -143,6 +143,7 @@ function getActiveTraceWriter() {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// src/services/command-helpers.ts
|
|
146
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
146
147
|
var EXIT_CREDENTIAL_STORE_UNAVAILABLE = 4;
|
|
147
148
|
function reportCredentialStoreUnavailable(err) {
|
|
148
149
|
if (!(err instanceof CredentialStoreUnavailableError)) return false;
|
|
@@ -249,8 +250,9 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
249
250
|
);
|
|
250
251
|
writeErrorDetail(msg, "PERMISSION_DENIED");
|
|
251
252
|
} else if (msg.startsWith("NOT_FOUND")) {
|
|
253
|
+
const subject = id === void 0 ? "Project or resource" : `Work item ${id}`;
|
|
252
254
|
process.stderr.write(
|
|
253
|
-
`Error:
|
|
255
|
+
`Error: ${subject} not found in ${context?.org}/${context?.project}.
|
|
254
256
|
`
|
|
255
257
|
);
|
|
256
258
|
} else if (msg === "NETWORK_ERROR") {
|
|
@@ -278,11 +280,29 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
278
280
|
process.exitCode = 1;
|
|
279
281
|
}
|
|
280
282
|
}
|
|
283
|
+
var STDIN_PATH = "-";
|
|
284
|
+
function readTextSource(file) {
|
|
285
|
+
if (file === STDIN_PATH) {
|
|
286
|
+
try {
|
|
287
|
+
return readFileSync2(0, "utf-8");
|
|
288
|
+
} catch {
|
|
289
|
+
throw new Error("Cannot read standard input.");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!existsSync(file)) {
|
|
293
|
+
throw new Error(`File not found: ${file}`);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return readFileSync2(file, "utf-8");
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(`Cannot read file: ${file}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
281
301
|
|
|
282
302
|
// src/services/image-download.ts
|
|
283
303
|
import { Jimp } from "jimp";
|
|
284
304
|
import { writeFile } from "fs/promises";
|
|
285
|
-
import { existsSync } from "fs";
|
|
305
|
+
import { existsSync as existsSync2 } from "fs";
|
|
286
306
|
import { tmpdir } from "os";
|
|
287
307
|
import { join } from "path";
|
|
288
308
|
var ATTACHMENT_GUID_RE = /_apis\/wit\/attachments\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
|
|
@@ -365,7 +385,7 @@ function resolveImageDownloadOptions(flags) {
|
|
|
365
385
|
maxWidth = parsed;
|
|
366
386
|
}
|
|
367
387
|
const outputDir = flags.imagesPath ?? tmpdir();
|
|
368
|
-
if (flags.imagesPath !== void 0 && !
|
|
388
|
+
if (flags.imagesPath !== void 0 && !existsSync2(outputDir)) {
|
|
369
389
|
throw new Error(`Images path "${outputDir}" does not exist.`);
|
|
370
390
|
}
|
|
371
391
|
return { enabled, maxWidth, outputDir };
|
|
@@ -455,6 +475,9 @@ var DEFAULT_FIELDS = [
|
|
|
455
475
|
"System.State",
|
|
456
476
|
"System.WorkItemType",
|
|
457
477
|
"System.AssignedTo",
|
|
478
|
+
"System.CreatedBy",
|
|
479
|
+
"System.CreatedDate",
|
|
480
|
+
"System.Tags",
|
|
458
481
|
"System.Description",
|
|
459
482
|
"Microsoft.VSTS.Common.AcceptanceCriteria",
|
|
460
483
|
"Microsoft.VSTS.TCM.ReproSteps",
|
|
@@ -667,12 +690,19 @@ function buildWorkItemCommentsUrl(context, id) {
|
|
|
667
690
|
url.searchParams.set("api-version", "7.1-preview.4");
|
|
668
691
|
return url;
|
|
669
692
|
}
|
|
693
|
+
function buildWorkItemCommentUrl(context, id, commentId) {
|
|
694
|
+
const url = buildWorkItemCommentsUrl(context, id);
|
|
695
|
+
url.pathname = `${url.pathname}/${commentId}`;
|
|
696
|
+
return url;
|
|
697
|
+
}
|
|
670
698
|
function mapWorkItemComment(comment, fallbackWorkItemId) {
|
|
671
699
|
return {
|
|
672
700
|
id: comment.id ?? comment.commentId ?? 0,
|
|
673
701
|
workItemId: comment.workItemId ?? fallbackWorkItemId,
|
|
674
702
|
text: typeof comment.text === "string" ? comment.text : "",
|
|
675
703
|
author: comment.createdBy?.displayName ?? null,
|
|
704
|
+
authorUniqueName: comment.createdBy?.uniqueName ?? null,
|
|
705
|
+
authorId: comment.createdBy?.id ?? null,
|
|
676
706
|
createdAt: comment.createdDate ?? null,
|
|
677
707
|
modifiedAt: comment.modifiedDate ?? null,
|
|
678
708
|
isDeleted: comment.isDeleted === true
|
|
@@ -782,6 +812,30 @@ async function getOrgFieldNames(context, cred) {
|
|
|
782
812
|
const data = await response.json();
|
|
783
813
|
return (data.value ?? []).map((f) => f.referenceName);
|
|
784
814
|
}
|
|
815
|
+
function toIdentity(ref) {
|
|
816
|
+
if (!ref) return null;
|
|
817
|
+
return {
|
|
818
|
+
displayName: ref.displayName ?? null,
|
|
819
|
+
uniqueName: ref.uniqueName ?? null,
|
|
820
|
+
id: ref.id ?? null
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
var PULL_REQUEST_ARTIFACT = /^vstfs:\/\/\/Git\/PullRequestId\/(.+)$/i;
|
|
824
|
+
function mapRelations(relations) {
|
|
825
|
+
return (relations ?? []).map((r) => {
|
|
826
|
+
const entry = { rel: r.rel, name: r.attributes?.name ?? null, url: r.url };
|
|
827
|
+
const workItemMatch = /\/workItems\/(\d+)$/i.exec(r.url);
|
|
828
|
+
if (workItemMatch) entry.workItemId = Number(workItemMatch[1]);
|
|
829
|
+
const prMatch = PULL_REQUEST_ARTIFACT.exec(r.url);
|
|
830
|
+
if (prMatch) {
|
|
831
|
+
const [projectId, repositoryId, prId] = prMatch[1].split(/%2F|\//i);
|
|
832
|
+
if (projectId && repositoryId && /^\d+$/.test(prId ?? "")) {
|
|
833
|
+
entry.pullRequest = { id: Number(prId), repositoryId, projectId };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return entry;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
785
839
|
function buildCombinedDescription(fields) {
|
|
786
840
|
const parts = [];
|
|
787
841
|
if (fields["System.Description"]) {
|
|
@@ -837,6 +891,11 @@ async function getWorkItem(context, id, cred, extraFields) {
|
|
|
837
891
|
state: data.fields["System.State"],
|
|
838
892
|
type: data.fields["System.WorkItemType"],
|
|
839
893
|
assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
|
|
894
|
+
assignedToIdentity: toIdentity(data.fields["System.AssignedTo"]),
|
|
895
|
+
createdBy: toIdentity(data.fields["System.CreatedBy"]),
|
|
896
|
+
createdDate: data.fields["System.CreatedDate"] ?? null,
|
|
897
|
+
tags: parseTags(data.fields["System.Tags"]),
|
|
898
|
+
relations: mapRelations(relationsData.relations),
|
|
840
899
|
description: buildCombinedDescription(data.fields),
|
|
841
900
|
areaPath: data.fields["System.AreaPath"],
|
|
842
901
|
iterationPath: data.fields["System.IterationPath"],
|
|
@@ -891,16 +950,11 @@ async function listWorkItemComments(context, id, cred) {
|
|
|
891
950
|
comments
|
|
892
951
|
};
|
|
893
952
|
}
|
|
894
|
-
async function
|
|
895
|
-
const url = buildWorkItemCommentsUrl(context, id);
|
|
896
|
-
url.searchParams.set("format", format);
|
|
953
|
+
async function sendCommentRequest(url, cred, method, body) {
|
|
897
954
|
const response = await fetchWithErrors(url.toString(), {
|
|
898
|
-
method
|
|
899
|
-
headers: {
|
|
900
|
-
|
|
901
|
-
"Content-Type": "application/json"
|
|
902
|
-
},
|
|
903
|
-
body: JSON.stringify({ text })
|
|
955
|
+
method,
|
|
956
|
+
headers: body === void 0 ? authHeaders(cred) : { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
957
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
904
958
|
});
|
|
905
959
|
if (response.status === 400) {
|
|
906
960
|
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
@@ -909,16 +963,54 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
|
909
963
|
if (!response.ok) {
|
|
910
964
|
throw httpError(response);
|
|
911
965
|
}
|
|
966
|
+
return response;
|
|
967
|
+
}
|
|
968
|
+
async function writeCommentText(url, cred, method, text, format, fallback) {
|
|
969
|
+
url.searchParams.set("format", format);
|
|
970
|
+
const response = await sendCommentRequest(url, cred, method, { text });
|
|
912
971
|
const data = await response.json();
|
|
913
972
|
return {
|
|
914
|
-
workItemId: data.workItemId ??
|
|
915
|
-
commentId: data.commentId ?? data.id ??
|
|
973
|
+
workItemId: data.workItemId ?? fallback.workItemId,
|
|
974
|
+
commentId: data.commentId ?? data.id ?? fallback.commentId,
|
|
916
975
|
text: typeof data.text === "string" ? data.text : text,
|
|
917
976
|
author: data.createdBy?.displayName ?? null,
|
|
977
|
+
authorUniqueName: data.createdBy?.uniqueName ?? null,
|
|
978
|
+
authorId: data.createdBy?.id ?? null,
|
|
918
979
|
createdAt: data.createdDate ?? null,
|
|
980
|
+
modifiedAt: data.modifiedDate ?? null,
|
|
919
981
|
url: data.url ?? null
|
|
920
982
|
};
|
|
921
983
|
}
|
|
984
|
+
async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
985
|
+
const written = await writeCommentText(
|
|
986
|
+
buildWorkItemCommentsUrl(context, id),
|
|
987
|
+
cred,
|
|
988
|
+
"POST",
|
|
989
|
+
text,
|
|
990
|
+
format,
|
|
991
|
+
{ workItemId: id, commentId: 0 }
|
|
992
|
+
);
|
|
993
|
+
return {
|
|
994
|
+
workItemId: written.workItemId,
|
|
995
|
+
commentId: written.commentId,
|
|
996
|
+
text: written.text,
|
|
997
|
+
author: written.author,
|
|
998
|
+
authorUniqueName: written.authorUniqueName,
|
|
999
|
+
authorId: written.authorId,
|
|
1000
|
+
createdAt: written.createdAt,
|
|
1001
|
+
url: written.url
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
async function updateWorkItemComment(context, id, commentId, cred, text, format = "html") {
|
|
1005
|
+
return writeCommentText(buildWorkItemCommentUrl(context, id, commentId), cred, "PATCH", text, format, {
|
|
1006
|
+
workItemId: id,
|
|
1007
|
+
commentId
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
async function deleteWorkItemComment(context, id, commentId, cred) {
|
|
1011
|
+
await sendCommentRequest(buildWorkItemCommentUrl(context, id, commentId), cred, "DELETE");
|
|
1012
|
+
return { workItemId: id, commentId, deleted: true };
|
|
1013
|
+
}
|
|
922
1014
|
async function updateWorkItem(context, id, cred, fieldName, operations) {
|
|
923
1015
|
const result = await applyWorkItemPatch(context, id, cred, operations);
|
|
924
1016
|
const title = result.fields["System.Title"];
|
|
@@ -988,10 +1080,111 @@ async function createAttachment(context, fileName, content, cred) {
|
|
|
988
1080
|
}
|
|
989
1081
|
return await response.json();
|
|
990
1082
|
}
|
|
1083
|
+
var LIST_SYSTEM_FIELDS = [
|
|
1084
|
+
"System.Title",
|
|
1085
|
+
"System.State",
|
|
1086
|
+
"System.WorkItemType",
|
|
1087
|
+
"System.AssignedTo",
|
|
1088
|
+
"System.Description",
|
|
1089
|
+
"System.Tags",
|
|
1090
|
+
"System.TeamProject"
|
|
1091
|
+
];
|
|
1092
|
+
var LIST_PROCESS_FIELDS = [
|
|
1093
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria",
|
|
1094
|
+
"Microsoft.VSTS.TCM.ReproSteps"
|
|
1095
|
+
];
|
|
1096
|
+
var WORKITEMS_BATCH_LIMIT = 200;
|
|
1097
|
+
function wiqlLiteral(value) {
|
|
1098
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
1099
|
+
}
|
|
1100
|
+
function buildListWiql(filter) {
|
|
1101
|
+
const clauses = ["[System.TeamProject] = @project"];
|
|
1102
|
+
if (filter.state) clauses.push(`[System.State] = ${wiqlLiteral(filter.state)}`);
|
|
1103
|
+
if (filter.tag) clauses.push(`[System.Tags] CONTAINS ${wiqlLiteral(filter.tag)}`);
|
|
1104
|
+
if (filter.assignedTo) {
|
|
1105
|
+
clauses.push(
|
|
1106
|
+
filter.assignedTo.toLowerCase() === "@me" ? "[System.AssignedTo] = @Me" : `[System.AssignedTo] = ${wiqlLiteral(filter.assignedTo)}`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
if (filter.titleContains) clauses.push(`[System.Title] CONTAINS ${wiqlLiteral(filter.titleContains)}`);
|
|
1110
|
+
return `SELECT [System.Id] FROM WorkItems WHERE ${clauses.join(" AND ")} ORDER BY [System.ChangedDate] DESC`;
|
|
1111
|
+
}
|
|
1112
|
+
function parseTags(raw) {
|
|
1113
|
+
if (typeof raw !== "string") return [];
|
|
1114
|
+
return raw.split(";").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1115
|
+
}
|
|
1116
|
+
async function runWiql(context, cred, wiql, top) {
|
|
1117
|
+
const url = new URL(
|
|
1118
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/wiql`
|
|
1119
|
+
);
|
|
1120
|
+
url.searchParams.set("api-version", "7.1");
|
|
1121
|
+
url.searchParams.set("$top", String(top));
|
|
1122
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
1123
|
+
method: "POST",
|
|
1124
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
1125
|
+
body: JSON.stringify({ query: wiql })
|
|
1126
|
+
});
|
|
1127
|
+
if (response.status === 400) {
|
|
1128
|
+
const serverMessage = await readResponseMessage(response);
|
|
1129
|
+
if (serverMessage) throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (!response.ok) throw httpError(response);
|
|
1132
|
+
const data = await response.json();
|
|
1133
|
+
return (data.workItems ?? []).map((w) => w.id);
|
|
1134
|
+
}
|
|
1135
|
+
async function postWorkItemsBatch(context, cred, ids, fields) {
|
|
1136
|
+
const url = new URL(
|
|
1137
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitemsbatch`
|
|
1138
|
+
);
|
|
1139
|
+
url.searchParams.set("api-version", "7.1");
|
|
1140
|
+
return fetchWithErrors(url.toString(), {
|
|
1141
|
+
method: "POST",
|
|
1142
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
1143
|
+
body: JSON.stringify({ ids, fields })
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
async function readWorkItemsBatch(context, cred, ids) {
|
|
1147
|
+
let response = await postWorkItemsBatch(context, cred, ids, [...LIST_SYSTEM_FIELDS, ...LIST_PROCESS_FIELDS]);
|
|
1148
|
+
if (response.status === 400) {
|
|
1149
|
+
const serverMessage = await readResponseMessage(response);
|
|
1150
|
+
if (!serverMessage?.includes("TF51535")) {
|
|
1151
|
+
throw serverMessage ? new Error(`BAD_REQUEST: ${serverMessage}`) : httpError(response);
|
|
1152
|
+
}
|
|
1153
|
+
response = await postWorkItemsBatch(context, cred, ids, LIST_SYSTEM_FIELDS);
|
|
1154
|
+
}
|
|
1155
|
+
if (!response.ok) throw httpError(response);
|
|
1156
|
+
return (await response.json()).value ?? [];
|
|
1157
|
+
}
|
|
1158
|
+
async function queryWorkItems(context, cred, filter) {
|
|
1159
|
+
const ids = await runWiql(context, cred, buildListWiql(filter), filter.top);
|
|
1160
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1161
|
+
const chunks = [];
|
|
1162
|
+
for (let i = 0; i < ids.length; i += WORKITEMS_BATCH_LIMIT) {
|
|
1163
|
+
chunks.push(ids.slice(i, i + WORKITEMS_BATCH_LIMIT));
|
|
1164
|
+
}
|
|
1165
|
+
const batches = await Promise.all(chunks.map((chunk) => readWorkItemsBatch(context, cred, chunk)));
|
|
1166
|
+
for (const batch of batches) {
|
|
1167
|
+
for (const item of batch) {
|
|
1168
|
+
const teamProject = item.fields["System.TeamProject"];
|
|
1169
|
+
const project = typeof teamProject === "string" ? teamProject : context.project;
|
|
1170
|
+
byId.set(item.id, {
|
|
1171
|
+
id: item.id,
|
|
1172
|
+
title: item.fields["System.Title"],
|
|
1173
|
+
description: buildCombinedDescription(item.fields),
|
|
1174
|
+
url: `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(project)}/_workitems/edit/${item.id}`,
|
|
1175
|
+
state: item.fields["System.State"],
|
|
1176
|
+
type: item.fields["System.WorkItemType"],
|
|
1177
|
+
tags: parseTags(item.fields["System.Tags"]),
|
|
1178
|
+
assignedTo: item.fields["System.AssignedTo"]?.displayName ?? null
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return ids.flatMap((id) => byId.get(id) ?? []);
|
|
1183
|
+
}
|
|
991
1184
|
|
|
992
1185
|
// src/services/auth.ts
|
|
993
1186
|
import { createInterface } from "readline";
|
|
994
|
-
import { existsSync as
|
|
1187
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
995
1188
|
import { dirname as dirname2, join as join2 } from "path";
|
|
996
1189
|
|
|
997
1190
|
// src/services/oauth-device-code.ts
|
|
@@ -1161,8 +1354,8 @@ async function promptForPat() {
|
|
|
1161
1354
|
});
|
|
1162
1355
|
}
|
|
1163
1356
|
function patFromEnvFile(envFile) {
|
|
1164
|
-
if (!
|
|
1165
|
-
const contents =
|
|
1357
|
+
if (!existsSync3(envFile)) return null;
|
|
1358
|
+
const contents = readFileSync3(envFile, "utf8");
|
|
1166
1359
|
for (const line of contents.split("\n")) {
|
|
1167
1360
|
const match = line.match(/^AZDO_PAT\s*=([^\n\r]+)$/);
|
|
1168
1361
|
if (match === null) continue;
|
|
@@ -1846,6 +2039,20 @@ function convertRichText(html, markdown) {
|
|
|
1846
2039
|
if (!html) return "";
|
|
1847
2040
|
return markdown ? toMarkdown(html) : stripHtml(html);
|
|
1848
2041
|
}
|
|
2042
|
+
function toJsonDocument(workItem) {
|
|
2043
|
+
return {
|
|
2044
|
+
id: workItem.id,
|
|
2045
|
+
title: workItem.title,
|
|
2046
|
+
description: workItem.description ? toMarkdown(workItem.description) : "",
|
|
2047
|
+
state: workItem.state,
|
|
2048
|
+
tags: workItem.tags ?? [],
|
|
2049
|
+
assignedTo: workItem.assignedToIdentity ?? null,
|
|
2050
|
+
createdBy: workItem.createdBy ?? null,
|
|
2051
|
+
createdDate: workItem.createdDate ?? null,
|
|
2052
|
+
url: workItem.url,
|
|
2053
|
+
relations: workItem.relations ?? []
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
1849
2056
|
function formatMarkdownField(fieldLabel, value) {
|
|
1850
2057
|
if (value.includes("\n")) {
|
|
1851
2058
|
return `${fieldLabel}:
|
|
@@ -1923,13 +2130,17 @@ function formatWorkItem(workItem, short, markdown = false) {
|
|
|
1923
2130
|
}
|
|
1924
2131
|
function createGetItemCommand() {
|
|
1925
2132
|
const command = new Command("get-item");
|
|
1926
|
-
command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown");
|
|
2133
|
+
command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown").option("--json", "output { id, title, description (markdown), state, tags, assignedTo, createdBy, createdDate, url, relations } (assignedTo/createdBy: { displayName, uniqueName, id }; relations include pull requests)");
|
|
1927
2134
|
addImageDownloadOptions(command);
|
|
1928
2135
|
command.action(
|
|
1929
2136
|
async (idStr, options) => {
|
|
1930
2137
|
const id = parseWorkItemId(idStr);
|
|
1931
2138
|
validateOrgProjectPair(options);
|
|
1932
2139
|
const imageOptions = resolveImageDownloadOptionsOrExit(options);
|
|
2140
|
+
if (options.json && (imageOptions.enabled || options.downloadImages || options.resizeImages !== void 0 || options.imagesPath !== void 0)) {
|
|
2141
|
+
process.stderr.write("Error: --json cannot be combined with image download options.\n");
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
}
|
|
1933
2144
|
let context;
|
|
1934
2145
|
try {
|
|
1935
2146
|
context = resolveContext(options);
|
|
@@ -1937,6 +2148,10 @@ function createGetItemCommand() {
|
|
|
1937
2148
|
const scopedCfg = resolveScopedConfig(context.org);
|
|
1938
2149
|
const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
|
|
1939
2150
|
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
2151
|
+
if (options.json) {
|
|
2152
|
+
process.stdout.write(JSON.stringify(toJsonDocument(workItem)) + "\n");
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
1940
2155
|
const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
|
|
1941
2156
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1942
2157
|
process.stdout.write(output + "\n");
|
|
@@ -1957,10 +2172,69 @@ function createGetItemCommand() {
|
|
|
1957
2172
|
return command;
|
|
1958
2173
|
}
|
|
1959
2174
|
|
|
1960
|
-
// src/commands/
|
|
2175
|
+
// src/commands/list-items.ts
|
|
1961
2176
|
import { Command as Command2 } from "commander";
|
|
2177
|
+
var DEFAULT_TOP = 50;
|
|
2178
|
+
var MAX_TOP = 1e3;
|
|
2179
|
+
function parseTop(raw) {
|
|
2180
|
+
if (raw === void 0) return DEFAULT_TOP;
|
|
2181
|
+
const top = Number(raw);
|
|
2182
|
+
if (!/^\d+$/.test(raw) || top < 1 || top > MAX_TOP) {
|
|
2183
|
+
process.stderr.write(`Error: --top must be an integer between 1 and ${MAX_TOP}. Got: "${raw}"
|
|
2184
|
+
`);
|
|
2185
|
+
process.exit(1);
|
|
2186
|
+
}
|
|
2187
|
+
return top;
|
|
2188
|
+
}
|
|
2189
|
+
function toJsonRow(item) {
|
|
2190
|
+
return {
|
|
2191
|
+
id: item.id,
|
|
2192
|
+
title: item.title,
|
|
2193
|
+
description: item.description ? toMarkdown(item.description) : "",
|
|
2194
|
+
url: item.url,
|
|
2195
|
+
state: item.state,
|
|
2196
|
+
tags: item.tags,
|
|
2197
|
+
assignedTo: item.assignedTo
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
function formatItemLine(item) {
|
|
2201
|
+
const tags = item.tags.length > 0 ? ` [${item.tags.join(", ")}]` : "";
|
|
2202
|
+
return `${item.id} ${item.state} ${item.assignedTo ?? "Unassigned"} ${item.title}${tags}`;
|
|
2203
|
+
}
|
|
2204
|
+
function createListItemsCommand() {
|
|
2205
|
+
const command = new Command2("list-items");
|
|
2206
|
+
command.description("List work items filtered by state, tag, assignee and title (one WIQL query, then batched reads of 200 items)").option("--state <state>", 'only items in this state (e.g. "Active")').option("--tag <tag>", "only items carrying this tag").option("--assigned-to <user>", "only items assigned to this user (display name, email, or @me)").option("--title-contains <text>", "only items whose title contains this text").option("--top <n>", `maximum number of items, newest change first (default ${DEFAULT_TOP}, max ${MAX_TOP})`).option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output an array of { id, title, description (markdown), url, state, tags, assignedTo }").action(async (options) => {
|
|
2207
|
+
validateOrgProjectPair(options);
|
|
2208
|
+
const top = parseTop(options.top);
|
|
2209
|
+
let context;
|
|
2210
|
+
try {
|
|
2211
|
+
context = resolveContext(options);
|
|
2212
|
+
const credential = await requireAuthCredential(context.org);
|
|
2213
|
+
const items = await queryWorkItems(context, credential, {
|
|
2214
|
+
state: options.state,
|
|
2215
|
+
tag: options.tag,
|
|
2216
|
+
assignedTo: options.assignedTo,
|
|
2217
|
+
titleContains: options.titleContains,
|
|
2218
|
+
top
|
|
2219
|
+
});
|
|
2220
|
+
if (options.json) {
|
|
2221
|
+
process.stdout.write(JSON.stringify(items.map(toJsonRow)) + "\n");
|
|
2222
|
+
} else if (items.length === 0) {
|
|
2223
|
+
process.stdout.write("No work items found.\n");
|
|
2224
|
+
} else {
|
|
2225
|
+
process.stdout.write(items.map(formatItemLine).join("\n") + "\n");
|
|
2226
|
+
}
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
handleCommandError(err, void 0, context, "read");
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
return command;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/commands/clear-pat.ts
|
|
2235
|
+
import { Command as Command3 } from "commander";
|
|
1962
2236
|
function createClearPatCommand() {
|
|
1963
|
-
const command = new
|
|
2237
|
+
const command = new Command3("clear-pat");
|
|
1964
2238
|
command.description("Remove a stored Azure DevOps PAT (deprecated: use `azdo auth logout`)").option("--org <name>", "Azure DevOps organization (overrides auto-detect / config)").action(async (options) => {
|
|
1965
2239
|
process.stderr.write("`azdo clear-pat` is deprecated; use `azdo auth logout [--org <name>]` instead.\n");
|
|
1966
2240
|
const resolved = resolveOrg({ org: options.org });
|
|
@@ -1983,7 +2257,7 @@ function createClearPatCommand() {
|
|
|
1983
2257
|
}
|
|
1984
2258
|
|
|
1985
2259
|
// src/commands/auth.ts
|
|
1986
|
-
import { Command as
|
|
2260
|
+
import { Command as Command4 } from "commander";
|
|
1987
2261
|
|
|
1988
2262
|
// src/services/auth-diagnostics.ts
|
|
1989
2263
|
async function runConnectivityTest(org, cred) {
|
|
@@ -2481,7 +2755,7 @@ async function handleToken(orgFromGlobal) {
|
|
|
2481
2755
|
`);
|
|
2482
2756
|
}
|
|
2483
2757
|
function createAuthCommand() {
|
|
2484
|
-
const command = new
|
|
2758
|
+
const command = new Command4("auth");
|
|
2485
2759
|
command.description(
|
|
2486
2760
|
"Manage Azure DevOps authentication. Use `azdo auth login` for OAuth (default); the bare `azdo auth` form preserves the legacy PAT-prompt path for back-compat."
|
|
2487
2761
|
);
|
|
@@ -2619,7 +2893,7 @@ not be resolved, 4 OS credential store unavailable. stdout stays empty on all.
|
|
|
2619
2893
|
}
|
|
2620
2894
|
|
|
2621
2895
|
// src/commands/config.ts
|
|
2622
|
-
import { Command as
|
|
2896
|
+
import { Command as Command5 } from "commander";
|
|
2623
2897
|
import { createInterface as createInterface2 } from "readline";
|
|
2624
2898
|
|
|
2625
2899
|
// src/services/dpapi-copy.ts
|
|
@@ -2777,9 +3051,43 @@ async function promptForSetting(cfg, setting, ask) {
|
|
|
2777
3051
|
|
|
2778
3052
|
`);
|
|
2779
3053
|
}
|
|
3054
|
+
var SETTING_TYPE_LABEL = {
|
|
3055
|
+
string: "string",
|
|
3056
|
+
"string[]": "comma-separated list",
|
|
3057
|
+
boolean: "true | false"
|
|
3058
|
+
};
|
|
3059
|
+
function describeSettingValues(setting) {
|
|
3060
|
+
if (!setting.values) return `type: ${SETTING_TYPE_LABEL[setting.type]}`;
|
|
3061
|
+
const list = setting.values.map((v) => v.note ? `${v.value} (${v.note})` : v.value).join(", ");
|
|
3062
|
+
return `values: ${list}`;
|
|
3063
|
+
}
|
|
3064
|
+
function renderSettingsHelp(settings = SETTINGS) {
|
|
3065
|
+
const keyWidth = Math.max(...settings.map((s) => s.key.length)) + 3;
|
|
3066
|
+
const indent = " ".repeat(2 + keyWidth);
|
|
3067
|
+
const lines = ["Settings (azdo config set <key> <value>):", ""];
|
|
3068
|
+
for (const setting of settings) {
|
|
3069
|
+
const facts = [
|
|
3070
|
+
describeSettingValues(setting),
|
|
3071
|
+
`scope: ${setting.scoped ? "global, or per organization with --org <org>" : "global only"}`,
|
|
3072
|
+
...setting.required ? ["required"] : [],
|
|
3073
|
+
...setting.env ? [`env: ${setting.env} overrides the stored value`] : [],
|
|
3074
|
+
`example: azdo config set ${setting.key} ${setting.example}`
|
|
3075
|
+
];
|
|
3076
|
+
lines.push(
|
|
3077
|
+
` ${setting.key.padEnd(keyWidth)}${setting.description}`,
|
|
3078
|
+
...facts.map((fact) => `${indent}${fact}`),
|
|
3079
|
+
""
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
return lines.join("\n").trimEnd();
|
|
3083
|
+
}
|
|
3084
|
+
var KEY_ARGUMENT_HELP = `setting key (${SETTINGS.map((s) => s.key).join(", ")})`;
|
|
3085
|
+
var KEY_HELP_POINTER = "\nRun `azdo config --help` for each setting's meaning, accepted values and scope.\n";
|
|
2780
3086
|
function createConfigCommand() {
|
|
2781
|
-
const config = new
|
|
3087
|
+
const config = new Command5("config");
|
|
2782
3088
|
config.description("Manage CLI settings");
|
|
3089
|
+
config.addHelpText("after", `
|
|
3090
|
+
${renderSettingsHelp()}`);
|
|
2783
3091
|
config.addHelpText(
|
|
2784
3092
|
"after",
|
|
2785
3093
|
[
|
|
@@ -2796,8 +3104,8 @@ function createConfigCommand() {
|
|
|
2796
3104
|
"Run `azdo auth diagnose` to see which credential is in use."
|
|
2797
3105
|
].join("\n")
|
|
2798
3106
|
);
|
|
2799
|
-
const set = new
|
|
2800
|
-
set.description("Set a configuration value").argument("<key>",
|
|
3107
|
+
const set = new Command5("set");
|
|
3108
|
+
set.description("Set a configuration value").argument("<key>", KEY_ARGUMENT_HELP).argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").option("--copy-credentials", "with credentialStore dpapi: copy Credential Manager credentials without asking").option("--no-copy-credentials", "with credentialStore dpapi: do not offer to copy Credential Manager credentials").addHelpText("after", KEY_HELP_POINTER).action((key, value, options) => {
|
|
2801
3109
|
try {
|
|
2802
3110
|
if (options.org) {
|
|
2803
3111
|
setOrgScopedValue(options.org, key, value);
|
|
@@ -2830,8 +3138,8 @@ function createConfigCommand() {
|
|
|
2830
3138
|
}
|
|
2831
3139
|
writeJson();
|
|
2832
3140
|
});
|
|
2833
|
-
const get = new
|
|
2834
|
-
get.description("Get a configuration value").argument("<key>",
|
|
3141
|
+
const get = new Command5("get");
|
|
3142
|
+
get.description("Get a configuration value").argument("<key>", KEY_ARGUMENT_HELP).option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").addHelpText("after", KEY_HELP_POINTER).action((key, options) => {
|
|
2835
3143
|
try {
|
|
2836
3144
|
const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
|
|
2837
3145
|
if (options.json) {
|
|
@@ -2853,7 +3161,7 @@ function createConfigCommand() {
|
|
|
2853
3161
|
process.exit(1);
|
|
2854
3162
|
}
|
|
2855
3163
|
});
|
|
2856
|
-
const list = new
|
|
3164
|
+
const list = new Command5("list");
|
|
2857
3165
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
2858
3166
|
const cfg = loadConfig();
|
|
2859
3167
|
if (options.json) {
|
|
@@ -2863,8 +3171,8 @@ function createConfigCommand() {
|
|
|
2863
3171
|
}
|
|
2864
3172
|
writeConfigList(cfg);
|
|
2865
3173
|
});
|
|
2866
|
-
const unset = new
|
|
2867
|
-
unset.description("Remove a configuration value").argument("<key>",
|
|
3174
|
+
const unset = new Command5("unset");
|
|
3175
|
+
unset.description("Remove a configuration value").argument("<key>", KEY_ARGUMENT_HELP).option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").addHelpText("after", KEY_HELP_POINTER).action((key, options) => {
|
|
2868
3176
|
try {
|
|
2869
3177
|
if (options.org) {
|
|
2870
3178
|
unsetOrgScopedValue(options.org, key);
|
|
@@ -2885,7 +3193,7 @@ function createConfigCommand() {
|
|
|
2885
3193
|
process.exit(1);
|
|
2886
3194
|
}
|
|
2887
3195
|
});
|
|
2888
|
-
const wizard = new
|
|
3196
|
+
const wizard = new Command5("wizard");
|
|
2889
3197
|
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
2890
3198
|
if (!process.stdin.isTTY) {
|
|
2891
3199
|
process.stderr.write(
|
|
@@ -2908,7 +3216,7 @@ function createConfigCommand() {
|
|
|
2908
3216
|
rl.close();
|
|
2909
3217
|
process.stderr.write("Configuration complete!\n");
|
|
2910
3218
|
});
|
|
2911
|
-
const orgCopy = new
|
|
3219
|
+
const orgCopy = new Command5("org-copy");
|
|
2912
3220
|
orgCopy.description('Copy settings from one scope to another (use "default" as source to copy top-level settings)').argument("<from>", 'source scope name or "default"').argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2913
3221
|
try {
|
|
2914
3222
|
copyOrgScope(from, to, options.force ?? false);
|
|
@@ -2921,7 +3229,7 @@ function createConfigCommand() {
|
|
|
2921
3229
|
process.exit(1);
|
|
2922
3230
|
}
|
|
2923
3231
|
});
|
|
2924
|
-
const orgMove = new
|
|
3232
|
+
const orgMove = new Command5("org-move");
|
|
2925
3233
|
orgMove.description("Move settings from one org scope to another").argument("<from>", "source org name").argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2926
3234
|
try {
|
|
2927
3235
|
moveOrgScope(from, to, options.force ?? false);
|
|
@@ -2934,7 +3242,7 @@ function createConfigCommand() {
|
|
|
2934
3242
|
process.exit(1);
|
|
2935
3243
|
}
|
|
2936
3244
|
});
|
|
2937
|
-
const orgDelete = new
|
|
3245
|
+
const orgDelete = new Command5("org-delete");
|
|
2938
3246
|
orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
|
|
2939
3247
|
try {
|
|
2940
3248
|
deleteOrgScope(name);
|
|
@@ -2959,9 +3267,9 @@ function createConfigCommand() {
|
|
|
2959
3267
|
}
|
|
2960
3268
|
|
|
2961
3269
|
// src/commands/set-state.ts
|
|
2962
|
-
import { Command as
|
|
3270
|
+
import { Command as Command6 } from "commander";
|
|
2963
3271
|
function createSetStateCommand() {
|
|
2964
|
-
const command = new
|
|
3272
|
+
const command = new Command6("set-state");
|
|
2965
3273
|
command.description("Change the state of a work item").argument("<id>", "work item ID").argument("<state>", 'target state (e.g., "Active", "Closed")').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
2966
3274
|
async (idStr, state, options) => {
|
|
2967
3275
|
const id = parseWorkItemId(idStr);
|
|
@@ -2997,9 +3305,9 @@ function createSetStateCommand() {
|
|
|
2997
3305
|
}
|
|
2998
3306
|
|
|
2999
3307
|
// src/commands/assign.ts
|
|
3000
|
-
import { Command as
|
|
3308
|
+
import { Command as Command7 } from "commander";
|
|
3001
3309
|
function createAssignCommand() {
|
|
3002
|
-
const command = new
|
|
3310
|
+
const command = new Command7("assign");
|
|
3003
3311
|
command.description("Assign a work item to a user, or unassign it").argument("<id>", "work item ID").argument("[name]", "user display name or email").option("--unassign", "clear the Assigned To field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
3004
3312
|
async (idStr, name, options) => {
|
|
3005
3313
|
const id = parseWorkItemId(idStr);
|
|
@@ -3049,9 +3357,9 @@ function createAssignCommand() {
|
|
|
3049
3357
|
}
|
|
3050
3358
|
|
|
3051
3359
|
// src/commands/set-field.ts
|
|
3052
|
-
import { Command as
|
|
3360
|
+
import { Command as Command8 } from "commander";
|
|
3053
3361
|
function createSetFieldCommand() {
|
|
3054
|
-
const command = new
|
|
3362
|
+
const command = new Command8("set-field");
|
|
3055
3363
|
command.description("Set any work item field by its reference name").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Title)").argument("<value>", "new value for the field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
3056
3364
|
async (idStr, field, value, options) => {
|
|
3057
3365
|
const id = parseWorkItemId(idStr);
|
|
@@ -3087,9 +3395,9 @@ function createSetFieldCommand() {
|
|
|
3087
3395
|
}
|
|
3088
3396
|
|
|
3089
3397
|
// src/commands/get-md-field.ts
|
|
3090
|
-
import { Command as
|
|
3398
|
+
import { Command as Command9 } from "commander";
|
|
3091
3399
|
function createGetMdFieldCommand() {
|
|
3092
|
-
const command = new
|
|
3400
|
+
const command = new Command9("get-md-field");
|
|
3093
3401
|
command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project");
|
|
3094
3402
|
addImageDownloadOptions(command);
|
|
3095
3403
|
command.action(
|
|
@@ -3123,8 +3431,8 @@ function createGetMdFieldCommand() {
|
|
|
3123
3431
|
}
|
|
3124
3432
|
|
|
3125
3433
|
// src/commands/set-md-field.ts
|
|
3126
|
-
import { existsSync as
|
|
3127
|
-
import { Command as
|
|
3434
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
3435
|
+
import { Command as Command10 } from "commander";
|
|
3128
3436
|
function fail(message) {
|
|
3129
3437
|
process.stderr.write(`Error: ${message}
|
|
3130
3438
|
`);
|
|
@@ -3143,11 +3451,11 @@ function resolveContent(inlineContent, options) {
|
|
|
3143
3451
|
return null;
|
|
3144
3452
|
}
|
|
3145
3453
|
function readFileContent(filePath) {
|
|
3146
|
-
if (!
|
|
3454
|
+
if (!existsSync4(filePath)) {
|
|
3147
3455
|
fail(`File not found: ${filePath}`);
|
|
3148
3456
|
}
|
|
3149
3457
|
try {
|
|
3150
|
-
return
|
|
3458
|
+
return readFileSync4(filePath, "utf-8");
|
|
3151
3459
|
} catch {
|
|
3152
3460
|
fail(`Cannot read file: ${filePath}`);
|
|
3153
3461
|
}
|
|
@@ -3186,7 +3494,7 @@ function formatOutput(result, options, field) {
|
|
|
3186
3494
|
}
|
|
3187
3495
|
}
|
|
3188
3496
|
function createSetMdFieldCommand() {
|
|
3189
|
-
const command = new
|
|
3497
|
+
const command = new Command10("set-md-field");
|
|
3190
3498
|
command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
|
|
3191
3499
|
async (idStr, field, inlineContent, options) => {
|
|
3192
3500
|
const id = parseWorkItemId(idStr);
|
|
@@ -3212,8 +3520,8 @@ function createSetMdFieldCommand() {
|
|
|
3212
3520
|
}
|
|
3213
3521
|
|
|
3214
3522
|
// src/commands/upsert.ts
|
|
3215
|
-
import { existsSync as
|
|
3216
|
-
import { Command as
|
|
3523
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
3524
|
+
import { Command as Command11 } from "commander";
|
|
3217
3525
|
|
|
3218
3526
|
// src/services/task-document.ts
|
|
3219
3527
|
var FIELD_ALIASES = /* @__PURE__ */ new Map([
|
|
@@ -3376,12 +3684,12 @@ function loadSourceContent(options) {
|
|
|
3376
3684
|
return { content: options.content };
|
|
3377
3685
|
}
|
|
3378
3686
|
const filePath = options.file;
|
|
3379
|
-
if (!
|
|
3687
|
+
if (!existsSync5(filePath)) {
|
|
3380
3688
|
fail2(`File not found: ${filePath}`);
|
|
3381
3689
|
}
|
|
3382
3690
|
try {
|
|
3383
3691
|
return {
|
|
3384
|
-
content:
|
|
3692
|
+
content: readFileSync5(filePath, "utf-8"),
|
|
3385
3693
|
sourceFile: filePath
|
|
3386
3694
|
};
|
|
3387
3695
|
} catch {
|
|
@@ -3498,7 +3806,7 @@ function handleUpsertError(err, id, context) {
|
|
|
3498
3806
|
process.exit(1);
|
|
3499
3807
|
}
|
|
3500
3808
|
function createUpsertCommand() {
|
|
3501
|
-
const command = new
|
|
3809
|
+
const command = new Command11("upsert");
|
|
3502
3810
|
command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
3503
3811
|
validateOrgProjectPair(options);
|
|
3504
3812
|
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
@@ -3539,7 +3847,7 @@ function createUpsertCommand() {
|
|
|
3539
3847
|
}
|
|
3540
3848
|
|
|
3541
3849
|
// src/commands/list-fields.ts
|
|
3542
|
-
import { Command as
|
|
3850
|
+
import { Command as Command12 } from "commander";
|
|
3543
3851
|
function stringifyValue(value) {
|
|
3544
3852
|
if (value === null || value === void 0) return "";
|
|
3545
3853
|
if (typeof value === "string") return value;
|
|
@@ -3573,7 +3881,7 @@ function formatFieldList(fields) {
|
|
|
3573
3881
|
}).join("\n");
|
|
3574
3882
|
}
|
|
3575
3883
|
function createListFieldsCommand() {
|
|
3576
|
-
const command = new
|
|
3884
|
+
const command = new Command12("list-fields");
|
|
3577
3885
|
command.description("List all fields of an Azure DevOps work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
3578
3886
|
async (idStr, options) => {
|
|
3579
3887
|
const id = parseWorkItemId(idStr);
|
|
@@ -3600,8 +3908,7 @@ function createListFieldsCommand() {
|
|
|
3600
3908
|
}
|
|
3601
3909
|
|
|
3602
3910
|
// src/commands/pr.ts
|
|
3603
|
-
import {
|
|
3604
|
-
import { Command as Command12 } from "commander";
|
|
3911
|
+
import { Command as Command13 } from "commander";
|
|
3605
3912
|
|
|
3606
3913
|
// src/services/pr-client.ts
|
|
3607
3914
|
function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
|
|
@@ -3670,7 +3977,12 @@ function mapPullRequest(context, repo, pullRequest) {
|
|
|
3670
3977
|
url: pullRequest._links?.web?.href ?? buildPullRequestWebUrl(context, repo, pullRequest.pullRequestId),
|
|
3671
3978
|
description: pullRequest.description?.trim() || null,
|
|
3672
3979
|
createdByUniqueName: pullRequest.createdBy?.uniqueName ?? null,
|
|
3673
|
-
createdById: pullRequest.createdBy?.id ?? null
|
|
3980
|
+
createdById: pullRequest.createdBy?.id ?? null,
|
|
3981
|
+
isDraft: pullRequest.isDraft ?? false,
|
|
3982
|
+
creationDate: pullRequest.creationDate ?? null,
|
|
3983
|
+
closedDate: pullRequest.closedDate ?? null,
|
|
3984
|
+
reviewers: (pullRequest.reviewers ?? []).map(mapReviewer),
|
|
3985
|
+
labels: (pullRequest.labels ?? []).filter((label) => label.active !== false).map((label) => label.name)
|
|
3674
3986
|
};
|
|
3675
3987
|
}
|
|
3676
3988
|
function mapPullRequestCheckName(status2) {
|
|
@@ -3769,6 +4081,8 @@ function mapComment(comment) {
|
|
|
3769
4081
|
return {
|
|
3770
4082
|
id: comment.id,
|
|
3771
4083
|
author: comment.author?.displayName ?? null,
|
|
4084
|
+
authorUniqueName: comment.author?.uniqueName ?? null,
|
|
4085
|
+
authorId: comment.author?.id ?? null,
|
|
3772
4086
|
content,
|
|
3773
4087
|
publishedAt: comment.publishedDate ?? null,
|
|
3774
4088
|
commentType: comment.commentType ?? null
|
|
@@ -3850,6 +4164,15 @@ async function listRepositoryPullRequests(context, repo, cred, opts) {
|
|
|
3850
4164
|
const data = await readJsonResponse(response);
|
|
3851
4165
|
return data.value.map((pullRequest) => mapPullRequest(context, repo, pullRequest));
|
|
3852
4166
|
}
|
|
4167
|
+
async function getPullRequestWorkItemIds(context, repo, cred, prId) {
|
|
4168
|
+
const url = new URL(
|
|
4169
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/workitems`
|
|
4170
|
+
);
|
|
4171
|
+
url.searchParams.set("api-version", "7.1");
|
|
4172
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
4173
|
+
const data = await readJsonResponse(response);
|
|
4174
|
+
return data.value.map((ref) => Number.parseInt(ref.id, 10)).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
|
|
4175
|
+
}
|
|
3853
4176
|
async function getPullRequestChecks(context, repo, cred, prId) {
|
|
3854
4177
|
const response = await fetchWithErrors(
|
|
3855
4178
|
buildPullRequestStatusesUrl(context, repo, prId).toString(),
|
|
@@ -3921,43 +4244,26 @@ function formatContributions(composed) {
|
|
|
3921
4244
|
function describeDescriptionBudget(composed) {
|
|
3922
4245
|
return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
|
|
3923
4246
|
}
|
|
3924
|
-
|
|
3925
|
-
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
3926
|
-
status: "active",
|
|
3927
|
-
targetBranch: "develop"
|
|
3928
|
-
});
|
|
3929
|
-
if (existing.length === 1) {
|
|
3930
|
-
return {
|
|
3931
|
-
branch: sourceBranch,
|
|
3932
|
-
targetBranch: "develop",
|
|
3933
|
-
created: false,
|
|
3934
|
-
pullRequest: existing[0]
|
|
3935
|
-
};
|
|
3936
|
-
}
|
|
3937
|
-
if (existing.length > 1) {
|
|
3938
|
-
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
|
|
3939
|
-
}
|
|
3940
|
-
const repository = await getRepository(context, repo, cred);
|
|
3941
|
-
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
3942
|
-
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
3943
|
-
const composed = composeDescription(description, template);
|
|
3944
|
-
if (composed === null) {
|
|
3945
|
-
throw new Error("DESCRIPTION_REQUIRED");
|
|
3946
|
-
}
|
|
3947
|
-
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
3948
|
-
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
3949
|
-
}
|
|
4247
|
+
function buildOpenPayload(sourceBranch, targetBranch, title, description, options) {
|
|
3950
4248
|
const payload = {
|
|
3951
4249
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
3952
|
-
targetRefName:
|
|
4250
|
+
targetRefName: `refs/heads/${targetBranch}`,
|
|
3953
4251
|
title,
|
|
3954
|
-
description
|
|
4252
|
+
description
|
|
3955
4253
|
};
|
|
4254
|
+
if (options.isDraft) {
|
|
4255
|
+
payload.isDraft = true;
|
|
4256
|
+
}
|
|
4257
|
+
if (options.labels && options.labels.length > 0) {
|
|
4258
|
+
payload.labels = options.labels.map((name) => ({ name }));
|
|
4259
|
+
}
|
|
4260
|
+
return payload;
|
|
4261
|
+
}
|
|
4262
|
+
async function createPullRequest(context, repo, cred, payload, composed) {
|
|
3956
4263
|
const url = new URL(
|
|
3957
4264
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
3958
4265
|
);
|
|
3959
4266
|
url.searchParams.set("api-version", "7.1");
|
|
3960
|
-
let data;
|
|
3961
4267
|
try {
|
|
3962
4268
|
const response = await fetchWithErrors(url.toString(), {
|
|
3963
4269
|
method: "POST",
|
|
@@ -3967,19 +4273,69 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3967
4273
|
},
|
|
3968
4274
|
body: JSON.stringify(payload)
|
|
3969
4275
|
});
|
|
3970
|
-
|
|
4276
|
+
return await readJsonResponse(response);
|
|
3971
4277
|
} catch (err) {
|
|
3972
4278
|
if (err instanceof Error && err.message.startsWith("HTTP_400")) {
|
|
3973
4279
|
throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
|
|
3974
4280
|
}
|
|
3975
4281
|
throw err;
|
|
3976
4282
|
}
|
|
3977
|
-
|
|
4283
|
+
}
|
|
4284
|
+
function linkWorkItems(context, repo, cred, prId, workItemIds) {
|
|
4285
|
+
return Promise.all(
|
|
4286
|
+
workItemIds.map(
|
|
4287
|
+
(id) => linkWorkItemToPullRequest(context, repo, cred, prId, id).then(
|
|
4288
|
+
() => ({ id, linked: true }),
|
|
4289
|
+
(err) => ({ id, linked: false, error: err instanceof Error ? err.message : String(err) })
|
|
4290
|
+
)
|
|
4291
|
+
)
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
async function openPullRequest(context, repo, cred, sourceBranch, title, description, options = {}) {
|
|
4295
|
+
const targetBranch = options.targetBranch ?? "develop";
|
|
4296
|
+
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
4297
|
+
status: "active",
|
|
4298
|
+
targetBranch
|
|
4299
|
+
});
|
|
4300
|
+
if (existing.length === 1) {
|
|
4301
|
+
return {
|
|
4302
|
+
branch: sourceBranch,
|
|
4303
|
+
targetBranch,
|
|
4304
|
+
created: false,
|
|
4305
|
+
pullRequest: existing[0]
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
if (existing.length > 1) {
|
|
4309
|
+
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest2) => pullRequest2.id).join(",")}`);
|
|
4310
|
+
}
|
|
4311
|
+
const repository = await getRepository(context, repo, cred);
|
|
4312
|
+
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
4313
|
+
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch);
|
|
4314
|
+
const composed = composeDescription(description, template);
|
|
4315
|
+
if (composed === null) {
|
|
4316
|
+
throw new Error("DESCRIPTION_REQUIRED");
|
|
4317
|
+
}
|
|
4318
|
+
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
4319
|
+
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
4320
|
+
}
|
|
4321
|
+
const data = await createPullRequest(
|
|
4322
|
+
context,
|
|
4323
|
+
repo,
|
|
4324
|
+
cred,
|
|
4325
|
+
buildOpenPayload(sourceBranch, targetBranch, title, composed.text, options),
|
|
4326
|
+
composed
|
|
4327
|
+
);
|
|
4328
|
+
const pullRequest = mapPullRequest(context, repo, data);
|
|
4329
|
+
const result = {
|
|
3978
4330
|
branch: sourceBranch,
|
|
3979
|
-
targetBranch
|
|
4331
|
+
targetBranch,
|
|
3980
4332
|
created: true,
|
|
3981
|
-
pullRequest
|
|
4333
|
+
pullRequest
|
|
3982
4334
|
};
|
|
4335
|
+
if (options.workItemIds && options.workItemIds.length > 0) {
|
|
4336
|
+
result.workItems = await linkWorkItems(context, repo, cred, pullRequest.id, options.workItemIds);
|
|
4337
|
+
}
|
|
4338
|
+
return result;
|
|
3983
4339
|
}
|
|
3984
4340
|
async function updatePullRequest(context, repo, cred, prId, fields) {
|
|
3985
4341
|
if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
|
|
@@ -4067,10 +4423,25 @@ async function updateThreadComment(context, repo, cred, prId, threadId, commentI
|
|
|
4067
4423
|
return {
|
|
4068
4424
|
id: data.id,
|
|
4069
4425
|
author: data.author?.displayName ?? null,
|
|
4426
|
+
authorUniqueName: data.author?.uniqueName ?? null,
|
|
4427
|
+
authorId: data.author?.id ?? null,
|
|
4070
4428
|
content: data.content ?? content,
|
|
4071
4429
|
publishedAt: data.publishedDate ?? null
|
|
4072
4430
|
};
|
|
4073
4431
|
}
|
|
4432
|
+
async function deleteThreadComment(context, repo, cred, prId, threadId, commentId) {
|
|
4433
|
+
const url = new URL(
|
|
4434
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments/${commentId}`
|
|
4435
|
+
);
|
|
4436
|
+
url.searchParams.set("api-version", "7.1");
|
|
4437
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
4438
|
+
method: "DELETE",
|
|
4439
|
+
headers: authHeaders(cred)
|
|
4440
|
+
});
|
|
4441
|
+
if (!response.ok) {
|
|
4442
|
+
throw httpError(response);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4074
4445
|
async function postThreadComment(context, repo, cred, prId, threadId, content) {
|
|
4075
4446
|
const response = await fetchWithErrors(buildThreadCommentUrl(context, repo, prId, threadId).toString(), {
|
|
4076
4447
|
method: "POST",
|
|
@@ -4084,6 +4455,8 @@ async function postThreadComment(context, repo, cred, prId, threadId, content) {
|
|
|
4084
4455
|
return {
|
|
4085
4456
|
id: data.id,
|
|
4086
4457
|
author: data.author?.displayName ?? null,
|
|
4458
|
+
authorUniqueName: data.author?.uniqueName ?? null,
|
|
4459
|
+
authorId: data.author?.id ?? null,
|
|
4087
4460
|
content: data.content ?? content,
|
|
4088
4461
|
publishedAt: data.publishedDate ?? null
|
|
4089
4462
|
};
|
|
@@ -4200,13 +4573,34 @@ function buildPullRequestReviewerUrl(context, repo, prId, reviewerId) {
|
|
|
4200
4573
|
url.searchParams.set("api-version", "7.1");
|
|
4201
4574
|
return url;
|
|
4202
4575
|
}
|
|
4576
|
+
function reviewerVoteState(vote) {
|
|
4577
|
+
switch (vote) {
|
|
4578
|
+
case 10:
|
|
4579
|
+
return "approved";
|
|
4580
|
+
case 5:
|
|
4581
|
+
return "approved-with-suggestions";
|
|
4582
|
+
case 0:
|
|
4583
|
+
return "no-vote";
|
|
4584
|
+
case -5:
|
|
4585
|
+
return "waiting-for-author";
|
|
4586
|
+
case -10:
|
|
4587
|
+
return "rejected";
|
|
4588
|
+
case 15:
|
|
4589
|
+
return "bypassed";
|
|
4590
|
+
default:
|
|
4591
|
+
return "unknown";
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4203
4594
|
function mapReviewer(data) {
|
|
4595
|
+
const vote = data.vote ?? 0;
|
|
4204
4596
|
return {
|
|
4205
4597
|
id: data.id,
|
|
4206
4598
|
displayName: data.displayName ?? null,
|
|
4207
4599
|
uniqueName: data.uniqueName ?? null,
|
|
4208
4600
|
isRequired: data.isRequired ?? false,
|
|
4209
|
-
vote
|
|
4601
|
+
vote,
|
|
4602
|
+
voteState: reviewerVoteState(vote),
|
|
4603
|
+
hasDeclined: data.hasDeclined ?? false
|
|
4210
4604
|
};
|
|
4211
4605
|
}
|
|
4212
4606
|
async function getPullRequestReviewers(context, repo, cred, prId) {
|
|
@@ -4258,11 +4652,11 @@ function branchTemplateSegments(branch) {
|
|
|
4258
4652
|
}
|
|
4259
4653
|
return segments;
|
|
4260
4654
|
}
|
|
4261
|
-
async function fetchRepositoryItemContent(context, repo, cred,
|
|
4655
|
+
async function fetchRepositoryItemContent(context, repo, cred, path5, branch) {
|
|
4262
4656
|
const url = new URL(
|
|
4263
4657
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/items`
|
|
4264
4658
|
);
|
|
4265
|
-
url.searchParams.set("path",
|
|
4659
|
+
url.searchParams.set("path", path5);
|
|
4266
4660
|
url.searchParams.set("versionDescriptor.version", branch);
|
|
4267
4661
|
url.searchParams.set("versionDescriptor.versionType", "branch");
|
|
4268
4662
|
url.searchParams.set("includeContent", "true");
|
|
@@ -4305,10 +4699,10 @@ function defaultTemplateCandidates() {
|
|
|
4305
4699
|
return paths;
|
|
4306
4700
|
}
|
|
4307
4701
|
async function firstMatchingTemplate(context, repo, cred, defaultBranch, candidates, kind) {
|
|
4308
|
-
for (const
|
|
4309
|
-
const content = await fetchRepositoryItemContent(context, repo, cred,
|
|
4702
|
+
for (const path5 of candidates) {
|
|
4703
|
+
const content = await fetchRepositoryItemContent(context, repo, cred, path5, defaultBranch);
|
|
4310
4704
|
if (content !== null) {
|
|
4311
|
-
return { path:
|
|
4705
|
+
return { path: path5, content, kind };
|
|
4312
4706
|
}
|
|
4313
4707
|
}
|
|
4314
4708
|
return null;
|
|
@@ -4334,7 +4728,7 @@ function parsePositivePrNumber(raw) {
|
|
|
4334
4728
|
return null;
|
|
4335
4729
|
}
|
|
4336
4730
|
const n = Number.parseInt(raw, 10);
|
|
4337
|
-
return Number.
|
|
4731
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
4338
4732
|
}
|
|
4339
4733
|
function prNumberHelp(branchStatus = "active") {
|
|
4340
4734
|
const matched = branchStatus === "abandoned" ? "abandoned" : "open";
|
|
@@ -4372,24 +4766,11 @@ function writeContractError(line) {
|
|
|
4372
4766
|
process.exitCode = 1;
|
|
4373
4767
|
}
|
|
4374
4768
|
var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
|
|
4375
|
-
|
|
4376
|
-
function readTextSource(file) {
|
|
4377
|
-
if (file === STDIN_PATH) {
|
|
4378
|
-
try {
|
|
4379
|
-
return readFileSync5(0, "utf-8");
|
|
4380
|
-
} catch {
|
|
4381
|
-
writeError("Cannot read standard input.");
|
|
4382
|
-
return null;
|
|
4383
|
-
}
|
|
4384
|
-
}
|
|
4385
|
-
if (!existsSync5(file)) {
|
|
4386
|
-
writeError(`File not found: ${file}`);
|
|
4387
|
-
return null;
|
|
4388
|
-
}
|
|
4769
|
+
function readTextSource2(file) {
|
|
4389
4770
|
try {
|
|
4390
|
-
return
|
|
4391
|
-
} catch {
|
|
4392
|
-
writeError(
|
|
4771
|
+
return readTextSource(file);
|
|
4772
|
+
} catch (err) {
|
|
4773
|
+
writeError(err.message);
|
|
4393
4774
|
return null;
|
|
4394
4775
|
}
|
|
4395
4776
|
}
|
|
@@ -4400,7 +4781,7 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4400
4781
|
}
|
|
4401
4782
|
let body;
|
|
4402
4783
|
if (file !== void 0) {
|
|
4403
|
-
const read =
|
|
4784
|
+
const read = readTextSource2(file);
|
|
4404
4785
|
if (read === null) {
|
|
4405
4786
|
return null;
|
|
4406
4787
|
}
|
|
@@ -4430,7 +4811,7 @@ function resolveOptionalTextInput(inline, file, flag) {
|
|
|
4430
4811
|
const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
|
|
4431
4812
|
let value;
|
|
4432
4813
|
if (file !== void 0) {
|
|
4433
|
-
const read =
|
|
4814
|
+
const read = readTextSource2(file);
|
|
4434
4815
|
if (read === null) {
|
|
4435
4816
|
return null;
|
|
4436
4817
|
}
|
|
@@ -4644,24 +5025,67 @@ async function resolvePrCommandContext(options, resolveOpts = {}) {
|
|
|
4644
5025
|
pat: credential
|
|
4645
5026
|
};
|
|
4646
5027
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
5028
|
+
var STATUS_PR_NUMBER_HELP = "show only the pull request with this numeric id, instead of the current branch's pull requests; mutually exclusive with --branch";
|
|
5029
|
+
var STATUS_BRANCH_HELP = "show the pull requests whose source branch is this one (with or without the refs/heads/ prefix), instead of the current branch's; no checkout needed. Mutually exclusive with --pr-number";
|
|
5030
|
+
function parseStatusTarget(options) {
|
|
5031
|
+
if (options.prNumber !== void 0 && options.branch !== void 0) {
|
|
5032
|
+
writeError("Cannot specify both --pr-number and --branch.");
|
|
5033
|
+
return null;
|
|
5034
|
+
}
|
|
5035
|
+
if (options.branch !== void 0) {
|
|
5036
|
+
const branch = options.branch.trim().replace(/^refs\/heads\//, "");
|
|
5037
|
+
if (branch === "") {
|
|
5038
|
+
writeError("--branch must not be empty.");
|
|
5039
|
+
return null;
|
|
5040
|
+
}
|
|
5041
|
+
return { kind: "branch", branch };
|
|
5042
|
+
}
|
|
5043
|
+
const prId = parseTargetPrNumber(options);
|
|
5044
|
+
if (prId === "invalid") {
|
|
5045
|
+
return null;
|
|
5046
|
+
}
|
|
5047
|
+
return prId === "none" ? { kind: "current" } : { kind: "pr", prId };
|
|
5048
|
+
}
|
|
5049
|
+
async function findStatusPullRequests(resolved, target) {
|
|
5050
|
+
if (target.kind === "pr") {
|
|
5051
|
+
const pullRequest = await fetchTargetById(resolved, target.prId);
|
|
5052
|
+
return pullRequest === null ? null : { branch: formatBranchName(pullRequest.sourceRefName), pullRequests: [pullRequest] };
|
|
5053
|
+
}
|
|
5054
|
+
const branch = target.kind === "branch" ? target.branch : resolved.branch;
|
|
5055
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
|
|
5056
|
+
if (target.kind === "branch" && pullRequests.length === 0) {
|
|
5057
|
+
writeError(
|
|
5058
|
+
`No pull requests found for branch ${branch} in ${resolved.context.org}/${resolved.context.project}/${resolved.repo}.`
|
|
5059
|
+
);
|
|
5060
|
+
return null;
|
|
5061
|
+
}
|
|
5062
|
+
return { branch, pullRequests };
|
|
5063
|
+
}
|
|
5064
|
+
function createPrStatusCommand() {
|
|
5065
|
+
const command = new Command13("status");
|
|
5066
|
+
withCommonPrOptions(configureUnwrappedHelp(command)).description("Check pull requests for the current branch, another branch, or a specific pull request").option("--pr-number <id>", STATUS_PR_NUMBER_HELP).option("--branch <name>", STATUS_BRANCH_HELP).option("--json", "output JSON").action(async (options) => {
|
|
5067
|
+
validateOrgProjectPair(options);
|
|
5068
|
+
const target = parseStatusTarget(options);
|
|
5069
|
+
if (target === null) {
|
|
5070
|
+
return;
|
|
5071
|
+
}
|
|
5072
|
+
let context;
|
|
5073
|
+
try {
|
|
5074
|
+
const resolved = await resolvePrCommandContext(options, { requireBranch: target.kind === "current" });
|
|
5075
|
+
context = resolved.context;
|
|
5076
|
+
const found = await findStatusPullRequests(resolved, target);
|
|
5077
|
+
if (found === null) {
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
5080
|
+
const { branch, pullRequests } = found;
|
|
5081
|
+
let projectId = null;
|
|
5082
|
+
try {
|
|
5083
|
+
projectId = await resolveProjectId(resolved.context, resolved.pat);
|
|
5084
|
+
} catch {
|
|
5085
|
+
projectId = null;
|
|
5086
|
+
}
|
|
5087
|
+
const pullRequestsWithChecks = await Promise.all(
|
|
5088
|
+
pullRequests.map(
|
|
4665
5089
|
async (pullRequest) => buildPullRequestStatusEntry(resolved.context, resolved.repo, resolved.pat, pullRequest, projectId)
|
|
4666
5090
|
)
|
|
4667
5091
|
);
|
|
@@ -4684,11 +5108,11 @@ function createPrStatusCommand() {
|
|
|
4684
5108
|
});
|
|
4685
5109
|
return command;
|
|
4686
5110
|
}
|
|
4687
|
-
function handlePrOpenError(err, context) {
|
|
5111
|
+
function handlePrOpenError(err, context, targetBranch = "develop") {
|
|
4688
5112
|
const message = err instanceof Error ? err.message : "";
|
|
4689
5113
|
if (message.startsWith("AMBIGUOUS_PRS:")) {
|
|
4690
5114
|
const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4691
|
-
writeError(`Multiple active pull requests already exist for this branch targeting
|
|
5115
|
+
writeError(`Multiple active pull requests already exist for this branch targeting ${targetBranch}: ${ids}. Use pr status to review them.`);
|
|
4692
5116
|
return;
|
|
4693
5117
|
}
|
|
4694
5118
|
if (message === "DESCRIPTION_REQUIRED") {
|
|
@@ -4701,13 +5125,54 @@ function handlePrOpenError(err, context) {
|
|
|
4701
5125
|
}
|
|
4702
5126
|
handlePrCommandError(err, context, "write");
|
|
4703
5127
|
}
|
|
5128
|
+
function collectOption(value, previous) {
|
|
5129
|
+
return [...previous, value];
|
|
5130
|
+
}
|
|
5131
|
+
function stripHeadsPrefix(branch) {
|
|
5132
|
+
return branch.trim().replace(/^refs\/heads\//, "");
|
|
5133
|
+
}
|
|
5134
|
+
function resolveOpenOptions(options) {
|
|
5135
|
+
const targetBranch = options.target === void 0 ? "develop" : stripHeadsPrefix(options.target);
|
|
5136
|
+
const source = options.source === void 0 ? void 0 : stripHeadsPrefix(options.source);
|
|
5137
|
+
if (!targetBranch) {
|
|
5138
|
+
writeError("--target must not be empty.");
|
|
5139
|
+
return null;
|
|
5140
|
+
}
|
|
5141
|
+
if (source !== void 0 && !source) {
|
|
5142
|
+
writeError("--source must not be empty.");
|
|
5143
|
+
return null;
|
|
5144
|
+
}
|
|
5145
|
+
const workItemIds = [];
|
|
5146
|
+
for (const raw of options.workItem ?? []) {
|
|
5147
|
+
const id = Number(raw.trim());
|
|
5148
|
+
if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(id) || id <= 0) {
|
|
5149
|
+
writeError(`--work-item must be a positive integer, got "${raw}".`);
|
|
5150
|
+
return null;
|
|
5151
|
+
}
|
|
5152
|
+
if (!workItemIds.includes(id)) {
|
|
5153
|
+
workItemIds.push(id);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const labels = [];
|
|
5157
|
+
for (const raw of options.label ?? []) {
|
|
5158
|
+
const name = raw.trim();
|
|
5159
|
+
if (!name) {
|
|
5160
|
+
writeError("--label must not be empty.");
|
|
5161
|
+
return null;
|
|
5162
|
+
}
|
|
5163
|
+
if (!labels.includes(name)) {
|
|
5164
|
+
labels.push(name);
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
return { targetBranch, source, isDraft: options.draft === true, labels, workItemIds };
|
|
5168
|
+
}
|
|
4704
5169
|
function resolveOpenDescription(inline, file) {
|
|
4705
5170
|
if (inline !== void 0 && file !== void 0) {
|
|
4706
5171
|
writeError("Cannot specify both --description and --description-file.");
|
|
4707
5172
|
return null;
|
|
4708
5173
|
}
|
|
4709
5174
|
if (file !== void 0) {
|
|
4710
|
-
const fromFile =
|
|
5175
|
+
const fromFile = readTextSource2(file);
|
|
4711
5176
|
if (fromFile === null) {
|
|
4712
5177
|
return null;
|
|
4713
5178
|
}
|
|
@@ -4725,14 +5190,19 @@ function resolveOpenDescription(inline, file) {
|
|
|
4725
5190
|
return trimmedInline;
|
|
4726
5191
|
}
|
|
4727
5192
|
function createPrOpenCommand() {
|
|
4728
|
-
const command = new
|
|
4729
|
-
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
5193
|
+
const command = new Command13("open");
|
|
5194
|
+
withCommonPrOptions(command).description("Open a pull request (default: from the current branch to develop)").option("--title <title>", "pull request title").option(
|
|
4730
5195
|
"--description <description>",
|
|
4731
5196
|
"pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
|
|
4732
5197
|
).option(
|
|
4733
5198
|
"--description-file <path>",
|
|
4734
5199
|
'read the description from a UTF-8 file instead of --description; "-" reads standard input'
|
|
4735
|
-
).option("--
|
|
5200
|
+
).option("--target <branch>", "target branch (default: develop); refs/heads/ prefix accepted").option("--source <branch>", "source branch (default: the current branch); no local checkout needed").option("--draft", "open the pull request as a draft").option(
|
|
5201
|
+
"--work-item <id>",
|
|
5202
|
+
"link a work item to the pull request (positive integer; repeatable)",
|
|
5203
|
+
collectOption,
|
|
5204
|
+
[]
|
|
5205
|
+
).option("--label <label>", "add a label to the pull request (repeatable)", collectOption, []).option("--json", "output JSON; adds top-level id and url next to pullRequest").action(async (options) => {
|
|
4736
5206
|
validateOrgProjectPair(options);
|
|
4737
5207
|
const title = options.title?.trim();
|
|
4738
5208
|
if (!title) {
|
|
@@ -4743,45 +5213,71 @@ function createPrOpenCommand() {
|
|
|
4743
5213
|
if (description === null) {
|
|
4744
5214
|
return;
|
|
4745
5215
|
}
|
|
5216
|
+
const openOptions = resolveOpenOptions(options);
|
|
5217
|
+
if (openOptions === null) {
|
|
5218
|
+
return;
|
|
5219
|
+
}
|
|
4746
5220
|
let context;
|
|
4747
5221
|
try {
|
|
4748
|
-
const
|
|
4749
|
-
|
|
4750
|
-
if (
|
|
4751
|
-
writeError(
|
|
5222
|
+
const openBranch = openOptions.source ?? getCurrentBranch();
|
|
5223
|
+
const target = openOptions.targetBranch;
|
|
5224
|
+
if (openBranch === target) {
|
|
5225
|
+
writeError(
|
|
5226
|
+
target === "develop" ? "Pull request creation requires a source branch other than develop." : `Pull request creation requires a source branch other than the target branch (${target}).`
|
|
5227
|
+
);
|
|
4752
5228
|
return;
|
|
4753
5229
|
}
|
|
4754
|
-
const
|
|
5230
|
+
const resolved = await resolvePrCommandContext(options, { requireBranch: false });
|
|
5231
|
+
context = resolved.context;
|
|
4755
5232
|
const result = await openPullRequest(
|
|
4756
5233
|
resolved.context,
|
|
4757
5234
|
resolved.repo,
|
|
4758
5235
|
resolved.pat,
|
|
4759
5236
|
openBranch,
|
|
4760
5237
|
title,
|
|
4761
|
-
description
|
|
5238
|
+
description,
|
|
5239
|
+
openOptions
|
|
4762
5240
|
);
|
|
4763
5241
|
if (options.json) {
|
|
4764
|
-
|
|
5242
|
+
const output = { ...result, id: result.pullRequest.id, url: result.pullRequest.url };
|
|
5243
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
4765
5244
|
`);
|
|
5245
|
+
reportWorkItemLinkFailures(result);
|
|
4766
5246
|
return;
|
|
4767
5247
|
}
|
|
4768
5248
|
if (result.created) {
|
|
4769
5249
|
process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
|
|
4770
5250
|
${result.pullRequest.url ?? "\u2014"}
|
|
4771
5251
|
`);
|
|
5252
|
+
reportWorkItemLinkFailures(result);
|
|
4772
5253
|
return;
|
|
4773
5254
|
}
|
|
4774
5255
|
process.stdout.write(
|
|
4775
|
-
`Active pull request already exists for ${
|
|
5256
|
+
`Active pull request already exists for ${openBranch} -> ${result.targetBranch}: #${result.pullRequest.id}
|
|
4776
5257
|
${result.pullRequest.url ?? "\u2014"}
|
|
4777
5258
|
`
|
|
4778
5259
|
);
|
|
5260
|
+
if (openOptions.isDraft || openOptions.labels?.length || openOptions.workItemIds?.length) {
|
|
5261
|
+
const hint = openOptions.workItemIds?.length ? " Use pr work-items link to link a work item." : "";
|
|
5262
|
+
process.stdout.write(
|
|
5263
|
+
`--draft, --label and --work-item were not applied; the existing pull request was left unchanged.${hint}
|
|
5264
|
+
`
|
|
5265
|
+
);
|
|
5266
|
+
}
|
|
4779
5267
|
} catch (err) {
|
|
4780
|
-
handlePrOpenError(err, context);
|
|
5268
|
+
handlePrOpenError(err, context, openOptions.targetBranch);
|
|
4781
5269
|
}
|
|
4782
5270
|
});
|
|
4783
5271
|
return command;
|
|
4784
5272
|
}
|
|
5273
|
+
function reportWorkItemLinkFailures(result) {
|
|
5274
|
+
const failed = (result.workItems ?? []).filter((item) => !item.linked);
|
|
5275
|
+
if (failed.length === 0) return;
|
|
5276
|
+
const detail = failed.map((item) => `#${item.id} (${item.error ?? "unknown error"})`).join(", ");
|
|
5277
|
+
writeError(
|
|
5278
|
+
`Pull request #${result.pullRequest.id} was created but could not be linked to work item ${detail}. Retry with: azdo pr work-items link <id> --pr-number ${result.pullRequest.id}`
|
|
5279
|
+
);
|
|
5280
|
+
}
|
|
4785
5281
|
function handlePrUpdateError(err, context) {
|
|
4786
5282
|
const message = err instanceof Error ? err.message : "";
|
|
4787
5283
|
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
@@ -4893,7 +5389,7 @@ ${updated.url ?? "\u2014"}
|
|
|
4893
5389
|
}
|
|
4894
5390
|
}
|
|
4895
5391
|
function createPrUpdateCommand() {
|
|
4896
|
-
const command = new
|
|
5392
|
+
const command = new Command13("update");
|
|
4897
5393
|
withCommonPrOptions(configureUnwrappedHelp(command)).alias("edit").description("Update a pull request's title and/or description").option("--pr-number <N>", PR_NUMBER_HELP).option("--title <title>", "new pull request title").option("--title-file <path>", 'read the new title from a UTF-8 file; "-" reads standard input').option(
|
|
4898
5394
|
"--description <description>",
|
|
4899
5395
|
'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
|
|
@@ -4981,7 +5477,7 @@ ${updated.url ?? "\u2014"}
|
|
|
4981
5477
|
}
|
|
4982
5478
|
}
|
|
4983
5479
|
function createPrAbandonCommand() {
|
|
4984
|
-
const command = new
|
|
5480
|
+
const command = new Command13("abandon");
|
|
4985
5481
|
withCommonPrOptions(configureUnwrappedHelp(command)).alias("close").description(
|
|
4986
5482
|
'Abandon a pull request (status: abandoned) \u2014 reversible with "azdo pr reactivate". This does NOT complete or merge the pull request; nothing is deleted and no confirmation is asked.'
|
|
4987
5483
|
).option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
|
|
@@ -4990,7 +5486,7 @@ function createPrAbandonCommand() {
|
|
|
4990
5486
|
return command;
|
|
4991
5487
|
}
|
|
4992
5488
|
function createPrReactivateCommand() {
|
|
4993
|
-
const command = new
|
|
5489
|
+
const command = new Command13("reactivate");
|
|
4994
5490
|
withCommonPrOptions(configureUnwrappedHelp(command)).description(
|
|
4995
5491
|
"Reactivate an abandoned pull request (status: active). When --pr-number is omitted the current branch's single ABANDONED pull request is used, not the active one."
|
|
4996
5492
|
).option("--pr-number <N>", PR_NUMBER_HELP_ABANDONED).option("--json", "output JSON").action(async (_options, command2) => {
|
|
@@ -5096,7 +5592,7 @@ function describeEmptyThreads(pullRequestId, allThreadCount, active) {
|
|
|
5096
5592
|
`;
|
|
5097
5593
|
}
|
|
5098
5594
|
function createPrCommentsCommand() {
|
|
5099
|
-
const command = new
|
|
5595
|
+
const command = new Command13("comments");
|
|
5100
5596
|
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) => {
|
|
5101
5597
|
validateOrgProjectPair(options);
|
|
5102
5598
|
const parsed = parseCommentsOptions(options);
|
|
@@ -5152,6 +5648,7 @@ function createPrCommentsCommand() {
|
|
|
5152
5648
|
command.addCommand(createPrCommentsReplyCommand());
|
|
5153
5649
|
command.addCommand(createPrCommentsAddCommand());
|
|
5154
5650
|
command.addCommand(createPrCommentsEditCommand());
|
|
5651
|
+
command.addCommand(createPrCommentsDeleteCommand());
|
|
5155
5652
|
return command;
|
|
5156
5653
|
}
|
|
5157
5654
|
function parseTargetPrNumber(options) {
|
|
@@ -5291,14 +5788,14 @@ async function runThreadStateChange(threadIdRaw, options, direction) {
|
|
|
5291
5788
|
}
|
|
5292
5789
|
}
|
|
5293
5790
|
function createPrCommentResolveCommand() {
|
|
5294
|
-
const command = new
|
|
5791
|
+
const command = new Command13("comment-resolve");
|
|
5295
5792
|
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) => {
|
|
5296
5793
|
await runThreadStateChange(threadIdRaw, options, "resolve");
|
|
5297
5794
|
});
|
|
5298
5795
|
return command;
|
|
5299
5796
|
}
|
|
5300
5797
|
function createPrCommentReopenCommand() {
|
|
5301
|
-
const command = new
|
|
5798
|
+
const command = new Command13("comment-reopen");
|
|
5302
5799
|
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) => {
|
|
5303
5800
|
await runThreadStateChange(threadIdRaw, options, "reopen");
|
|
5304
5801
|
});
|
|
@@ -5350,7 +5847,7 @@ async function runCommentReply(threadIdRaw, text, options) {
|
|
|
5350
5847
|
}
|
|
5351
5848
|
}
|
|
5352
5849
|
function buildCommentReplyCommand(name, description) {
|
|
5353
|
-
const command = new
|
|
5850
|
+
const command = new Command13(name);
|
|
5354
5851
|
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) => {
|
|
5355
5852
|
await runCommentReply(threadIdRaw, text, mergedPrOptions(command2));
|
|
5356
5853
|
});
|
|
@@ -5452,7 +5949,7 @@ ${body}
|
|
|
5452
5949
|
}
|
|
5453
5950
|
}
|
|
5454
5951
|
function buildCommentAddCommand(name, description) {
|
|
5455
|
-
const command = new
|
|
5952
|
+
const command = new Command13(name);
|
|
5456
5953
|
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(
|
|
5457
5954
|
"--status <status>",
|
|
5458
5955
|
`thread status (${CREATABLE_THREAD_STATUSES.join(" | ")}); omit for a plain, non-resolvable comment`
|
|
@@ -5470,7 +5967,26 @@ function createPrCommentAddCommand() {
|
|
|
5470
5967
|
'Post a new comment thread on the pull request overview (alias of "azdo pr comments add")'
|
|
5471
5968
|
);
|
|
5472
5969
|
}
|
|
5473
|
-
|
|
5970
|
+
function parseExplicitCommentId(options) {
|
|
5971
|
+
if (options.commentId === void 0) {
|
|
5972
|
+
return null;
|
|
5973
|
+
}
|
|
5974
|
+
const parsed = parsePositivePrNumber(options.commentId);
|
|
5975
|
+
if (parsed === null) {
|
|
5976
|
+
writeError(`Invalid --comment-id "${options.commentId}"; expected a positive integer.`);
|
|
5977
|
+
return "invalid";
|
|
5978
|
+
}
|
|
5979
|
+
return parsed;
|
|
5980
|
+
}
|
|
5981
|
+
function findCommentInThread(thread, commentId, target) {
|
|
5982
|
+
const match = thread.comments.find((comment) => comment.id === commentId);
|
|
5983
|
+
if (match === void 0) {
|
|
5984
|
+
writeError(`Comment #${commentId} not found in thread #${target.threadId} on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
|
|
5985
|
+
return null;
|
|
5986
|
+
}
|
|
5987
|
+
return match;
|
|
5988
|
+
}
|
|
5989
|
+
async function fetchTargetThread(target) {
|
|
5474
5990
|
try {
|
|
5475
5991
|
return await getPullRequestThread(
|
|
5476
5992
|
target.context,
|
|
@@ -5489,12 +6005,7 @@ async function fetchThreadForEdit(target) {
|
|
|
5489
6005
|
}
|
|
5490
6006
|
function selectEditableComment(thread, explicitCommentId, target) {
|
|
5491
6007
|
if (explicitCommentId !== null) {
|
|
5492
|
-
|
|
5493
|
-
if (match === void 0) {
|
|
5494
|
-
writeError(`Comment #${explicitCommentId} not found in thread #${target.threadId} on pull request #${target.pullRequest.id}.`, EXIT_NOT_FOUND);
|
|
5495
|
-
return null;
|
|
5496
|
-
}
|
|
5497
|
-
return match;
|
|
6008
|
+
return findCommentInThread(thread, explicitCommentId, target);
|
|
5498
6009
|
}
|
|
5499
6010
|
const first = [...thread.comments].sort((a, b) => a.id - b.id)[0];
|
|
5500
6011
|
if (first === void 0) {
|
|
@@ -5529,13 +6040,9 @@ async function runCommentEdit(threadIdRaw, text, options) {
|
|
|
5529
6040
|
if (body === null) {
|
|
5530
6041
|
return;
|
|
5531
6042
|
}
|
|
5532
|
-
|
|
5533
|
-
if (
|
|
5534
|
-
|
|
5535
|
-
if (explicitCommentId === null) {
|
|
5536
|
-
writeError(`Invalid --comment-id "${options.commentId}"; expected a positive integer.`);
|
|
5537
|
-
return;
|
|
5538
|
-
}
|
|
6043
|
+
const explicitCommentId = parseExplicitCommentId(options);
|
|
6044
|
+
if (explicitCommentId === "invalid") {
|
|
6045
|
+
return;
|
|
5539
6046
|
}
|
|
5540
6047
|
const target = await resolveThreadTarget(threadIdRaw, options, {
|
|
5541
6048
|
onContextResolved: (resolved) => {
|
|
@@ -5545,7 +6052,7 @@ async function runCommentEdit(threadIdRaw, text, options) {
|
|
|
5545
6052
|
if (target === null) {
|
|
5546
6053
|
return;
|
|
5547
6054
|
}
|
|
5548
|
-
const thread = await
|
|
6055
|
+
const thread = await fetchTargetThread(target);
|
|
5549
6056
|
if (thread === null) {
|
|
5550
6057
|
return;
|
|
5551
6058
|
}
|
|
@@ -5592,7 +6099,7 @@ async function runCommentEdit(threadIdRaw, text, options) {
|
|
|
5592
6099
|
}
|
|
5593
6100
|
}
|
|
5594
6101
|
function buildCommentEditCommand(name, description) {
|
|
5595
|
-
const command = new
|
|
6102
|
+
const command = new Command13(name);
|
|
5596
6103
|
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) => {
|
|
5597
6104
|
await runCommentEdit(threadIdRaw, text, mergedPrOptions(command2));
|
|
5598
6105
|
});
|
|
@@ -5607,44 +6114,176 @@ function createPrCommentEditCommand() {
|
|
|
5607
6114
|
'Edit an existing pull request comment in place (alias of "azdo pr comments edit")'
|
|
5608
6115
|
);
|
|
5609
6116
|
}
|
|
6117
|
+
function selectDeletableComment(thread, explicitCommentId, target) {
|
|
6118
|
+
if (explicitCommentId !== null) {
|
|
6119
|
+
return findCommentInThread(thread, explicitCommentId, target);
|
|
6120
|
+
}
|
|
6121
|
+
const comments = [...thread.comments].sort((a, b) => a.id - b.id);
|
|
6122
|
+
if (comments.length === 1) {
|
|
6123
|
+
return comments[0];
|
|
6124
|
+
}
|
|
6125
|
+
if (comments.length === 0) {
|
|
6126
|
+
writeError(`Thread #${target.threadId} on pull request #${target.pullRequest.id} has no comment to delete.`, EXIT_NOT_FOUND);
|
|
6127
|
+
return null;
|
|
6128
|
+
}
|
|
6129
|
+
const candidates = comments.map((comment) => `#${comment.id} (${comment.author ?? "Unknown"})`).join(", ");
|
|
6130
|
+
writeError(
|
|
6131
|
+
`Thread #${target.threadId} on pull request #${target.pullRequest.id} holds ${comments.length} comments; pass --comment-id to choose one: ${candidates}.`
|
|
6132
|
+
);
|
|
6133
|
+
return null;
|
|
6134
|
+
}
|
|
6135
|
+
function reportDeleteResult(result, json, comment) {
|
|
6136
|
+
if (json) {
|
|
6137
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6138
|
+
`);
|
|
6139
|
+
return;
|
|
6140
|
+
}
|
|
6141
|
+
if (result.dryRun) {
|
|
6142
|
+
process.stdout.write(
|
|
6143
|
+
`Dry run: would delete comment #${result.commentId} by ${comment.author ?? "Unknown"} (${comment.content.length} chars) from thread #${result.threadId} on pull request #${result.pullRequestId}.
|
|
6144
|
+
`
|
|
6145
|
+
);
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
process.stdout.write(
|
|
6149
|
+
`Comment #${result.commentId} deleted from thread #${result.threadId} on pull request #${result.pullRequestId}.
|
|
6150
|
+
`
|
|
6151
|
+
);
|
|
6152
|
+
}
|
|
6153
|
+
async function runCommentDelete(threadIdRaw, options) {
|
|
6154
|
+
let context;
|
|
6155
|
+
try {
|
|
6156
|
+
const explicitCommentId = parseExplicitCommentId(options);
|
|
6157
|
+
if (explicitCommentId === "invalid") {
|
|
6158
|
+
return;
|
|
6159
|
+
}
|
|
6160
|
+
const target = await resolveThreadTarget(threadIdRaw, options, {
|
|
6161
|
+
onContextResolved: (resolved) => {
|
|
6162
|
+
context = resolved;
|
|
6163
|
+
}
|
|
6164
|
+
});
|
|
6165
|
+
if (target === null) {
|
|
6166
|
+
return;
|
|
6167
|
+
}
|
|
6168
|
+
const thread = await fetchTargetThread(target);
|
|
6169
|
+
if (thread === null) {
|
|
6170
|
+
return;
|
|
6171
|
+
}
|
|
6172
|
+
const comment = selectDeletableComment(thread, explicitCommentId, target);
|
|
6173
|
+
if (comment === null) {
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
const dryRun = options.dryRun === true;
|
|
6177
|
+
if (!dryRun) {
|
|
6178
|
+
await deleteThreadComment(
|
|
6179
|
+
target.context,
|
|
6180
|
+
target.repo,
|
|
6181
|
+
target.pat,
|
|
6182
|
+
target.pullRequest.id,
|
|
6183
|
+
target.threadId,
|
|
6184
|
+
comment.id
|
|
6185
|
+
);
|
|
6186
|
+
}
|
|
6187
|
+
reportDeleteResult(
|
|
6188
|
+
{
|
|
6189
|
+
pullRequestId: target.pullRequest.id,
|
|
6190
|
+
threadId: target.threadId,
|
|
6191
|
+
commentId: comment.id,
|
|
6192
|
+
deleted: !dryRun,
|
|
6193
|
+
dryRun
|
|
6194
|
+
},
|
|
6195
|
+
options.json === true,
|
|
6196
|
+
comment
|
|
6197
|
+
);
|
|
6198
|
+
} catch (err) {
|
|
6199
|
+
handlePrCommandError(err, context, "write");
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
function buildCommentDeleteCommand(name, description) {
|
|
6203
|
+
const command = new Command13(name);
|
|
6204
|
+
withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<threadId>", "numeric id of the thread holding the comment").option("--comment-id <N>", "numeric id of the comment to delete; may be omitted only when the thread holds a single comment").option("--dry-run", "resolve the target comment and print what would be deleted, without deleting anything").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, _options, command2) => {
|
|
6205
|
+
await runCommentDelete(threadIdRaw, mergedPrOptions(command2));
|
|
6206
|
+
});
|
|
6207
|
+
return command;
|
|
6208
|
+
}
|
|
6209
|
+
function createPrCommentsDeleteCommand() {
|
|
6210
|
+
return buildCommentDeleteCommand("delete", "Delete a pull request comment (no confirmation prompt; irreversible)");
|
|
6211
|
+
}
|
|
6212
|
+
function createPrCommentDeleteCommand() {
|
|
6213
|
+
return buildCommentDeleteCommand(
|
|
6214
|
+
"comment-delete",
|
|
6215
|
+
'Delete a pull request comment (alias of "azdo pr comments delete"; no confirmation prompt; irreversible)'
|
|
6216
|
+
);
|
|
6217
|
+
}
|
|
5610
6218
|
var LIST_STATUS_VALUES = ["active", "completed", "abandoned", "all"];
|
|
5611
6219
|
var DEFAULT_LIST_TOP = 25;
|
|
6220
|
+
var WORK_ITEM_LOOKUP_CONCURRENCY = 5;
|
|
5612
6221
|
function formatPullRequestListEntry(pullRequest) {
|
|
5613
|
-
|
|
5614
|
-
|
|
6222
|
+
const state = pullRequest.isDraft ? `${pullRequest.status}, draft` : pullRequest.status;
|
|
6223
|
+
const lines = [
|
|
6224
|
+
`#${pullRequest.id} [${state}] ${pullRequest.title}`,
|
|
5615
6225
|
` ${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
|
|
5616
|
-
` Author: ${pullRequest.createdBy ?? "Unknown"}
|
|
5617
|
-
|
|
5618
|
-
|
|
6226
|
+
` Author: ${pullRequest.createdBy ?? "Unknown"}`
|
|
6227
|
+
];
|
|
6228
|
+
if ("workItemIds" in pullRequest) {
|
|
6229
|
+
const ids = pullRequest.workItemIds.map((id) => `#${id}`).join(", ");
|
|
6230
|
+
lines.push(` Work items: ${ids || "none"}`);
|
|
6231
|
+
}
|
|
6232
|
+
lines.push(` ${pullRequest.url ?? "\u2014"}`);
|
|
6233
|
+
return lines.join("\n");
|
|
6234
|
+
}
|
|
6235
|
+
function parseListOptions(options) {
|
|
6236
|
+
const status2 = options.status ?? "active";
|
|
6237
|
+
if (!LIST_STATUS_VALUES.includes(status2)) {
|
|
6238
|
+
writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
|
|
6239
|
+
return null;
|
|
6240
|
+
}
|
|
6241
|
+
let top = DEFAULT_LIST_TOP;
|
|
6242
|
+
if (options.top !== void 0) {
|
|
6243
|
+
const parsed = parsePositivePrNumber(options.top);
|
|
6244
|
+
if (parsed === null) {
|
|
6245
|
+
writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
|
|
6246
|
+
return null;
|
|
6247
|
+
}
|
|
6248
|
+
top = parsed;
|
|
6249
|
+
}
|
|
6250
|
+
const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
|
|
6251
|
+
return { status: status2, top, branch };
|
|
6252
|
+
}
|
|
6253
|
+
async function attachWorkItemIds(context, repo, cred, pullRequests) {
|
|
6254
|
+
const result = new Array(pullRequests.length);
|
|
6255
|
+
let next = 0;
|
|
6256
|
+
const worker = async () => {
|
|
6257
|
+
while (next < pullRequests.length) {
|
|
6258
|
+
const index = next;
|
|
6259
|
+
next += 1;
|
|
6260
|
+
const pullRequest = pullRequests[index];
|
|
6261
|
+
const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
|
|
6262
|
+
result[index] = { ...pullRequest, workItemIds };
|
|
6263
|
+
}
|
|
6264
|
+
};
|
|
6265
|
+
await Promise.all(Array.from({ length: Math.min(WORK_ITEM_LOOKUP_CONCURRENCY, pullRequests.length) }, worker));
|
|
6266
|
+
return result;
|
|
5619
6267
|
}
|
|
5620
6268
|
function createPrListCommand() {
|
|
5621
|
-
const command = new
|
|
5622
|
-
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) => {
|
|
6269
|
+
const command = new Command13("list");
|
|
6270
|
+
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("--work-items", "also return the ids of each pull request's linked work items (one extra API call per pull request)").option("--json", "output JSON").action(async (options) => {
|
|
5623
6271
|
validateOrgProjectPair(options);
|
|
5624
|
-
const
|
|
5625
|
-
if (
|
|
5626
|
-
writeError(`Invalid --status "${status2}"; expected one of ${LIST_STATUS_VALUES.join(", ")}.`);
|
|
6272
|
+
const parsed = parseListOptions(options);
|
|
6273
|
+
if (parsed === null) {
|
|
5627
6274
|
return;
|
|
5628
6275
|
}
|
|
5629
|
-
|
|
5630
|
-
if (options.top !== void 0) {
|
|
5631
|
-
const parsed = parsePositivePrNumber(options.top);
|
|
5632
|
-
if (parsed === null) {
|
|
5633
|
-
writeError(`Invalid --top "${options.top}"; expected a positive integer.`);
|
|
5634
|
-
return;
|
|
5635
|
-
}
|
|
5636
|
-
top = parsed;
|
|
5637
|
-
}
|
|
5638
|
-
const branch = options.branch?.trim().replace(/^refs\/heads\//, "") || null;
|
|
6276
|
+
const { status: status2, top, branch } = parsed;
|
|
5639
6277
|
let context;
|
|
5640
6278
|
try {
|
|
5641
6279
|
const resolved = await resolvePrCommandContext(options, { requireBranch: false });
|
|
5642
6280
|
context = resolved.context;
|
|
5643
|
-
const
|
|
6281
|
+
const listed = await listRepositoryPullRequests(resolved.context, resolved.repo, resolved.pat, {
|
|
5644
6282
|
sourceBranch: branch ?? void 0,
|
|
5645
6283
|
status: status2,
|
|
5646
6284
|
top
|
|
5647
6285
|
});
|
|
6286
|
+
const pullRequests = options.workItems ? await attachWorkItemIds(resolved.context, resolved.repo, resolved.pat, listed) : listed;
|
|
5648
6287
|
const result = {
|
|
5649
6288
|
repository: resolved.repo,
|
|
5650
6289
|
branch,
|
|
@@ -5725,14 +6364,14 @@ async function runWorkItemLinkChange(workItemIdRaw, options, direction) {
|
|
|
5725
6364
|
}
|
|
5726
6365
|
}
|
|
5727
6366
|
function buildWorkItemLinkCommand(name, description, direction) {
|
|
5728
|
-
const command = new
|
|
6367
|
+
const command = new Command13(name);
|
|
5729
6368
|
withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<workItemId>", "numeric id of the work item").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (workItemIdRaw, _options, command2) => {
|
|
5730
6369
|
await runWorkItemLinkChange(workItemIdRaw, mergedPrOptions(command2), direction);
|
|
5731
6370
|
});
|
|
5732
6371
|
return command;
|
|
5733
6372
|
}
|
|
5734
6373
|
function createPrWorkItemsCommand() {
|
|
5735
|
-
const command = new
|
|
6374
|
+
const command = new Command13("work-items");
|
|
5736
6375
|
command.description("Manage work items linked to a pull request");
|
|
5737
6376
|
command.addCommand(buildWorkItemLinkCommand("link", "Link a work item to the pull request", "link"));
|
|
5738
6377
|
command.addCommand(buildWorkItemLinkCommand("unlink", "Unlink a work item from the pull request", "unlink"));
|
|
@@ -5840,15 +6479,56 @@ async function runReviewerRemove(reviewer, options) {
|
|
|
5840
6479
|
handlePrCommandError(err, context, "write");
|
|
5841
6480
|
}
|
|
5842
6481
|
}
|
|
6482
|
+
function formatReviewerLine(reviewer) {
|
|
6483
|
+
const name = reviewer.displayName ?? reviewer.uniqueName ?? reviewer.id;
|
|
6484
|
+
const unique = reviewer.uniqueName && reviewer.uniqueName !== name ? ` <${reviewer.uniqueName}>` : "";
|
|
6485
|
+
const kind = reviewer.isRequired ? "required" : "optional";
|
|
6486
|
+
const declined = reviewer.hasDeclined ? ", declined" : "";
|
|
6487
|
+
return `${name}${unique} \u2014 ${reviewer.voteState} (${kind}${declined})`;
|
|
6488
|
+
}
|
|
6489
|
+
async function runReviewerList(options) {
|
|
6490
|
+
let context;
|
|
6491
|
+
try {
|
|
6492
|
+
const target = await resolvePullRequestTarget(options, {
|
|
6493
|
+
onContextResolved: (resolved) => {
|
|
6494
|
+
context = resolved;
|
|
6495
|
+
}
|
|
6496
|
+
});
|
|
6497
|
+
if (target === null) {
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6500
|
+
const reviewers = await getPullRequestReviewers(target.context, target.repo, target.pat, target.pullRequest.id);
|
|
6501
|
+
const result = { pullRequestId: target.pullRequest.id, reviewers };
|
|
6502
|
+
if (options.json) {
|
|
6503
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6504
|
+
`);
|
|
6505
|
+
return;
|
|
6506
|
+
}
|
|
6507
|
+
if (reviewers.length === 0) {
|
|
6508
|
+
process.stdout.write(`No reviewers on pull request #${target.pullRequest.id}.
|
|
6509
|
+
`);
|
|
6510
|
+
return;
|
|
6511
|
+
}
|
|
6512
|
+
process.stdout.write(`${reviewers.map(formatReviewerLine).join("\n")}
|
|
6513
|
+
`);
|
|
6514
|
+
} catch (err) {
|
|
6515
|
+
handlePrCommandError(err, context, "read");
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
5843
6518
|
function createPrReviewersCommand() {
|
|
5844
|
-
const command = new
|
|
6519
|
+
const command = new Command13("reviewers");
|
|
5845
6520
|
command.description("Manage pull request reviewers");
|
|
5846
|
-
const
|
|
6521
|
+
const list = new Command13("list");
|
|
6522
|
+
withCommonPrOptions(configureUnwrappedHelp(list)).description("List the pull request reviewers with their votes (approved, waiting-for-author, \u2026) and required flag").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (_options, command2) => {
|
|
6523
|
+
await runReviewerList(mergedPrOptions(command2));
|
|
6524
|
+
});
|
|
6525
|
+
command.addCommand(list);
|
|
6526
|
+
const add = new Command13("add");
|
|
5847
6527
|
withCommonPrOptions(configureUnwrappedHelp(add)).description("Add a reviewer to the pull request (optional by default)").argument("<reviewer>", "reviewer email or unique name").option("--required", "mark the reviewer as required instead of optional").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
|
|
5848
6528
|
await runReviewerAdd(reviewer, mergedPrOptions(command2));
|
|
5849
6529
|
});
|
|
5850
6530
|
command.addCommand(add);
|
|
5851
|
-
const remove = new
|
|
6531
|
+
const remove = new Command13("remove");
|
|
5852
6532
|
withCommonPrOptions(configureUnwrappedHelp(remove)).description("Remove a reviewer from the pull request").argument("<reviewer>", "reviewer email or unique name").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
|
|
5853
6533
|
await runReviewerRemove(reviewer, mergedPrOptions(command2));
|
|
5854
6534
|
});
|
|
@@ -5856,7 +6536,7 @@ function createPrReviewersCommand() {
|
|
|
5856
6536
|
return command;
|
|
5857
6537
|
}
|
|
5858
6538
|
function createPrCommand() {
|
|
5859
|
-
const command = new
|
|
6539
|
+
const command = new Command13("pr");
|
|
5860
6540
|
command.description("Manage Azure DevOps pull requests");
|
|
5861
6541
|
command.addCommand(createPrListCommand());
|
|
5862
6542
|
command.addCommand(createPrStatusCommand());
|
|
@@ -5872,11 +6552,13 @@ function createPrCommand() {
|
|
|
5872
6552
|
command.addCommand(createPrWorkItemsCommand());
|
|
5873
6553
|
command.addCommand(createPrReviewersCommand());
|
|
5874
6554
|
command.addCommand(createPrCommentEditCommand());
|
|
6555
|
+
command.addCommand(createPrCommentDeleteCommand());
|
|
5875
6556
|
return command;
|
|
5876
6557
|
}
|
|
5877
6558
|
|
|
5878
6559
|
// src/commands/pipeline.ts
|
|
5879
|
-
import { Command as
|
|
6560
|
+
import { Command as Command14 } from "commander";
|
|
6561
|
+
import path3 from "path";
|
|
5880
6562
|
|
|
5881
6563
|
// src/services/pipeline-client.ts
|
|
5882
6564
|
var API_VERSION = "7.1";
|
|
@@ -6000,6 +6682,9 @@ async function getBuildStatus(context, cred, buildId) {
|
|
|
6000
6682
|
const build = await getBuild(context, cred, buildId);
|
|
6001
6683
|
return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
|
|
6002
6684
|
}
|
|
6685
|
+
function recordErrors(record) {
|
|
6686
|
+
return (record.issues ?? []).filter((issue) => issue.type === "error" && issue.message).map((issue) => ({ message: issue.message ?? "", source: record.name ?? null }));
|
|
6687
|
+
}
|
|
6003
6688
|
async function getBuildTimeline(context, cred, buildId) {
|
|
6004
6689
|
const url = withApiVersion(
|
|
6005
6690
|
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
|
|
@@ -6011,14 +6696,20 @@ async function getBuildTimeline(context, cred, buildId) {
|
|
|
6011
6696
|
const stages = [];
|
|
6012
6697
|
const jobs = [];
|
|
6013
6698
|
const logSteps = /* @__PURE__ */ new Map();
|
|
6699
|
+
const logRecords = /* @__PURE__ */ new Map();
|
|
6700
|
+
const namesById = /* @__PURE__ */ new Map();
|
|
6014
6701
|
for (const record of records) {
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
}
|
|
6702
|
+
if (record.id && record.name) namesById.set(record.id, record.name);
|
|
6703
|
+
}
|
|
6704
|
+
for (const record of records) {
|
|
6705
|
+
errors.push(...recordErrors(record));
|
|
6020
6706
|
if (record.name && record.log?.id !== void 0) {
|
|
6021
6707
|
logSteps.set(record.log.id, record.name);
|
|
6708
|
+
logRecords.set(record.log.id, {
|
|
6709
|
+
name: record.name,
|
|
6710
|
+
type: record.type ?? null,
|
|
6711
|
+
parent: record.parentId && namesById.get(record.parentId) || null
|
|
6712
|
+
});
|
|
6022
6713
|
}
|
|
6023
6714
|
if (!record.name) continue;
|
|
6024
6715
|
const status2 = {
|
|
@@ -6038,7 +6729,7 @@ async function getBuildTimeline(context, cred, buildId) {
|
|
|
6038
6729
|
if (b.startTime === void 0) return -1;
|
|
6039
6730
|
return a.startTime < b.startTime ? -1 : 1;
|
|
6040
6731
|
});
|
|
6041
|
-
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
|
|
6732
|
+
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps, logRecords };
|
|
6042
6733
|
}
|
|
6043
6734
|
async function listTestRuns(context, cred, buildId) {
|
|
6044
6735
|
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
|
|
@@ -6147,16 +6838,18 @@ async function getRunLogs(context, cred, buildId) {
|
|
|
6147
6838
|
);
|
|
6148
6839
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6149
6840
|
const data = await readJsonResponse2(response);
|
|
6150
|
-
let
|
|
6841
|
+
let logRecords = /* @__PURE__ */ new Map();
|
|
6151
6842
|
try {
|
|
6152
|
-
|
|
6843
|
+
logRecords = (await getBuildTimeline(context, cred, buildId)).logRecords;
|
|
6153
6844
|
} catch {
|
|
6154
6845
|
}
|
|
6155
6846
|
return data.value.map((log) => ({
|
|
6156
6847
|
id: log.id,
|
|
6157
6848
|
createdOn: log.createdOn ?? null,
|
|
6158
6849
|
lineCount: log.lineCount ?? null,
|
|
6159
|
-
step:
|
|
6850
|
+
step: logRecords.get(log.id)?.name ?? null,
|
|
6851
|
+
type: logRecords.get(log.id)?.type ?? null,
|
|
6852
|
+
parent: logRecords.get(log.id)?.parent ?? null
|
|
6160
6853
|
}));
|
|
6161
6854
|
}
|
|
6162
6855
|
async function getRunLog(context, cred, buildId, logId) {
|
|
@@ -6169,6 +6862,122 @@ async function getRunLog(context, cred, buildId, logId) {
|
|
|
6169
6862
|
}
|
|
6170
6863
|
return response.text();
|
|
6171
6864
|
}
|
|
6865
|
+
async function listBuildArtifacts(context, cred, buildId) {
|
|
6866
|
+
const url = withApiVersion(
|
|
6867
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/artifacts`)
|
|
6868
|
+
);
|
|
6869
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6870
|
+
const data = await readJsonResponse2(response);
|
|
6871
|
+
return data.value.map((artifact) => {
|
|
6872
|
+
const size = Number.parseInt(artifact.resource?.properties?.artifactsize ?? "", 10);
|
|
6873
|
+
return {
|
|
6874
|
+
id: artifact.id,
|
|
6875
|
+
name: artifact.name,
|
|
6876
|
+
type: artifact.resource?.type ?? null,
|
|
6877
|
+
sizeBytes: Number.isFinite(size) ? size : null,
|
|
6878
|
+
downloadUrl: artifact.resource?.downloadUrl ?? null
|
|
6879
|
+
};
|
|
6880
|
+
});
|
|
6881
|
+
}
|
|
6882
|
+
var INITIAL_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
6883
|
+
async function collectBody(body, declared, onProgress, initialBytes = INITIAL_BUFFER_BYTES) {
|
|
6884
|
+
const hint = Number.isSafeInteger(declared) && declared > 0 ? declared : Infinity;
|
|
6885
|
+
let buffer = new Uint8Array(Math.min(hint, initialBytes));
|
|
6886
|
+
let received = 0;
|
|
6887
|
+
for await (const chunk of body) {
|
|
6888
|
+
const needed = received + chunk.length;
|
|
6889
|
+
if (needed > buffer.length) {
|
|
6890
|
+
let capacity = Math.max(buffer.length * 2, needed);
|
|
6891
|
+
if (hint >= needed) capacity = Math.min(capacity, hint);
|
|
6892
|
+
const grown = new Uint8Array(capacity);
|
|
6893
|
+
grown.set(buffer.subarray(0, received));
|
|
6894
|
+
buffer = grown;
|
|
6895
|
+
}
|
|
6896
|
+
buffer.set(chunk, received);
|
|
6897
|
+
received = needed;
|
|
6898
|
+
onProgress?.(received);
|
|
6899
|
+
}
|
|
6900
|
+
return buffer.subarray(0, received);
|
|
6901
|
+
}
|
|
6902
|
+
async function downloadArtifactZip(cred, artifact, onProgress) {
|
|
6903
|
+
if (!artifact.downloadUrl) {
|
|
6904
|
+
throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
|
|
6905
|
+
}
|
|
6906
|
+
const url = new URL(artifact.downloadUrl);
|
|
6907
|
+
url.searchParams.set("$format", "zip");
|
|
6908
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6909
|
+
if (!response.ok) {
|
|
6910
|
+
throw httpError(response);
|
|
6911
|
+
}
|
|
6912
|
+
if (!response.body) {
|
|
6913
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
6914
|
+
}
|
|
6915
|
+
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
6916
|
+
return collectBody(response.body, declared, onProgress);
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
// src/services/artifact-extract.ts
|
|
6920
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
|
|
6921
|
+
import path2 from "path";
|
|
6922
|
+
import { unzipSync } from "fflate";
|
|
6923
|
+
function entryTarget(entry, artifactName) {
|
|
6924
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
6925
|
+
if (normalized.endsWith("/")) return null;
|
|
6926
|
+
const prefix = `${artifactName}/`;
|
|
6927
|
+
const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
|
|
6928
|
+
const segments = relative.split("/");
|
|
6929
|
+
if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
|
|
6930
|
+
throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
|
|
6931
|
+
}
|
|
6932
|
+
return relative;
|
|
6933
|
+
}
|
|
6934
|
+
function assertNoSymlinkEscape(root, relative) {
|
|
6935
|
+
let current = root;
|
|
6936
|
+
for (const segment of relative.split("/")) {
|
|
6937
|
+
current = path2.join(current, segment);
|
|
6938
|
+
let isLink;
|
|
6939
|
+
try {
|
|
6940
|
+
isLink = lstatSync(current).isSymbolicLink();
|
|
6941
|
+
} catch {
|
|
6942
|
+
return;
|
|
6943
|
+
}
|
|
6944
|
+
if (isLink) {
|
|
6945
|
+
throw new Error(`Refusing to extract through symbolic link "${current}".`);
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
function extractArtifactZip(zip, destination, options) {
|
|
6950
|
+
const entries = unzipSync(zip);
|
|
6951
|
+
const targets = [];
|
|
6952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6953
|
+
for (const [entry, data] of Object.entries(entries)) {
|
|
6954
|
+
const relative = entryTarget(entry, options.artifactName);
|
|
6955
|
+
if (relative === null) continue;
|
|
6956
|
+
const key = path2.posix.normalize(relative);
|
|
6957
|
+
if (seen.has(key)) {
|
|
6958
|
+
throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
|
|
6959
|
+
}
|
|
6960
|
+
seen.add(key);
|
|
6961
|
+
targets.push({ relative: key, data });
|
|
6962
|
+
}
|
|
6963
|
+
if (!options.force) {
|
|
6964
|
+
const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
|
|
6965
|
+
if (existing) {
|
|
6966
|
+
throw new Error(
|
|
6967
|
+
`"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
|
|
6968
|
+
);
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
mkdirSync(destination, { recursive: true });
|
|
6972
|
+
const root = realpathSync(destination);
|
|
6973
|
+
for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
|
|
6974
|
+
for (const { relative, data } of targets) {
|
|
6975
|
+
const file = path2.join(root, relative);
|
|
6976
|
+
mkdirSync(path2.dirname(file), { recursive: true });
|
|
6977
|
+
writeFileSync(file, data);
|
|
6978
|
+
}
|
|
6979
|
+
return targets.map((t) => t.relative);
|
|
6980
|
+
}
|
|
6172
6981
|
|
|
6173
6982
|
// src/commands/pipeline.ts
|
|
6174
6983
|
var EXIT_FAILED = 1;
|
|
@@ -6251,7 +7060,7 @@ function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
|
|
|
6251
7060
|
).join("\n");
|
|
6252
7061
|
}
|
|
6253
7062
|
function createPipelineListCommand() {
|
|
6254
|
-
const command = new
|
|
7063
|
+
const command = new Command14("list");
|
|
6255
7064
|
command.description("List Azure DevOps pipeline definitions").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--filter <name>", "filter definitions by name (case-insensitive substring)").option("--json", "output JSON").action(async (options) => {
|
|
6256
7065
|
validateOrgProjectPair(options);
|
|
6257
7066
|
let context;
|
|
@@ -6323,7 +7132,7 @@ function parseGetRunsInputs(defIdRaw, options) {
|
|
|
6323
7132
|
return { defId, limit: limit ?? 10, prNumber };
|
|
6324
7133
|
}
|
|
6325
7134
|
function createPipelineGetRunsCommand() {
|
|
6326
|
-
const command = new
|
|
7135
|
+
const command = new Command14("get-runs");
|
|
6327
7136
|
command.description("List recent runs for a pipeline definition (newest first)").argument("[def_id]", "pipeline definition id (optional with --commit or --pr)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--limit <n>", "maximum number of runs to show (default 10)").option("--branch <branch>", "only show runs for this source branch").option("--commit <sha>", "only show runs that built this commit (full or abbreviated SHA)").option("--pr <number>", "only show runs for this pull request").option("--json", "output JSON").action(async (defIdRaw, options) => {
|
|
6328
7137
|
validateOrgProjectPair(options);
|
|
6329
7138
|
const inputs = parseGetRunsInputs(defIdRaw, options);
|
|
@@ -6378,7 +7187,7 @@ function applyWaitExitCode(result) {
|
|
|
6378
7187
|
}
|
|
6379
7188
|
}
|
|
6380
7189
|
function createPipelineWaitCommand() {
|
|
6381
|
-
const command = new
|
|
7190
|
+
const command = new Command14("wait");
|
|
6382
7191
|
command.description("Wait for a pipeline run to finish; exit code reflects the result (0 success, non-zero otherwise)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--timeout <seconds>", "maximum seconds to wait (default 1800)").option("--poll-interval <seconds>", "seconds between status checks (default 5)").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
6383
7192
|
validateOrgProjectPair(options);
|
|
6384
7193
|
const runId = parsePositiveId(runIdRaw);
|
|
@@ -6505,19 +7314,26 @@ function formatRunDetail(detail) {
|
|
|
6505
7314
|
...testRows(detail)
|
|
6506
7315
|
].join("\n");
|
|
6507
7316
|
}
|
|
7317
|
+
async function withRunContext(runIdRaw, options, body) {
|
|
7318
|
+
validateOrgProjectPair(options);
|
|
7319
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7320
|
+
if (runId === null) {
|
|
7321
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7322
|
+
return;
|
|
7323
|
+
}
|
|
7324
|
+
let context;
|
|
7325
|
+
try {
|
|
7326
|
+
const resolved = await resolvePipelineContext(options);
|
|
7327
|
+
context = resolved.context;
|
|
7328
|
+
await body(runId, resolved);
|
|
7329
|
+
} catch (err) {
|
|
7330
|
+
handlePipelineError(err, context);
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
6508
7333
|
function createPipelineGetRunDetailCommand() {
|
|
6509
|
-
const command = new
|
|
6510
|
-
command.description("Show a detailed summary of a single pipeline run (errors, failing tests, stages)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
|
|
6511
|
-
|
|
6512
|
-
const runId = parsePositiveId(runIdRaw);
|
|
6513
|
-
if (runId === null) {
|
|
6514
|
-
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
6515
|
-
return;
|
|
6516
|
-
}
|
|
6517
|
-
let context;
|
|
6518
|
-
try {
|
|
6519
|
-
const resolved = await resolvePipelineContext(options);
|
|
6520
|
-
context = resolved.context;
|
|
7334
|
+
const command = new Command14("get-run-detail");
|
|
7335
|
+
command.description("Show a detailed summary of a single pipeline run (errors, failing tests, stages)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
|
|
7336
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
6521
7337
|
const detail = await getRunDetail(resolved.context, resolved.cred, runId);
|
|
6522
7338
|
if (options.json) {
|
|
6523
7339
|
process.stdout.write(`${JSON.stringify(detail, null, 2)}
|
|
@@ -6526,10 +7342,8 @@ function createPipelineGetRunDetailCommand() {
|
|
|
6526
7342
|
}
|
|
6527
7343
|
process.stdout.write(`${formatRunDetail(detail)}
|
|
6528
7344
|
`);
|
|
6529
|
-
}
|
|
6530
|
-
|
|
6531
|
-
}
|
|
6532
|
-
});
|
|
7345
|
+
})
|
|
7346
|
+
);
|
|
6533
7347
|
return command;
|
|
6534
7348
|
}
|
|
6535
7349
|
function grepWithContext(lines, grep, context) {
|
|
@@ -6552,17 +7366,37 @@ function grepWithContext(lines, grep, context) {
|
|
|
6552
7366
|
}
|
|
6553
7367
|
return selected;
|
|
6554
7368
|
}
|
|
6555
|
-
|
|
7369
|
+
var PROGRESS_BAR = /(?<![\d.])\d+(?:\.\d+)? [KMGT]?i?B \/ \d+(?:\.\d+)? [KMGT]?i?B \[/g;
|
|
7370
|
+
var LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z /;
|
|
7371
|
+
function collapseGluedBars(line) {
|
|
7372
|
+
const starts = [...line.matchAll(PROGRESS_BAR)].map((m) => m.index ?? 0);
|
|
7373
|
+
if (starts.length < 2) return line;
|
|
7374
|
+
const prefix = LOG_TIMESTAMP.exec(line)?.[0] ?? "";
|
|
7375
|
+
return prefix + line.slice(starts.at(-1));
|
|
7376
|
+
}
|
|
7377
|
+
function collapseProgress(line) {
|
|
7378
|
+
let end = line.length;
|
|
7379
|
+
while (end > 0 && line[end - 1] === "\r") end--;
|
|
7380
|
+
return collapseGluedBars(line.slice(line.lastIndexOf("\r", end - 1) + 1, end));
|
|
7381
|
+
}
|
|
7382
|
+
function filterLogLines(content, filters) {
|
|
7383
|
+
const { grep, tail, head, contextLines: context } = filters;
|
|
6556
7384
|
let lines = content.split("\n");
|
|
6557
7385
|
if (lines.at(-1) === "") {
|
|
6558
7386
|
lines.pop();
|
|
6559
7387
|
}
|
|
7388
|
+
if (filters.noProgress) {
|
|
7389
|
+
lines = lines.map(collapseProgress);
|
|
7390
|
+
}
|
|
6560
7391
|
if (grep) {
|
|
6561
7392
|
lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
|
|
6562
7393
|
}
|
|
6563
7394
|
if (tail !== void 0 && lines.length > tail) {
|
|
6564
7395
|
lines = lines.slice(-tail);
|
|
6565
7396
|
}
|
|
7397
|
+
if (head !== void 0 && lines.length > head) {
|
|
7398
|
+
lines = lines.slice(0, head);
|
|
7399
|
+
}
|
|
6566
7400
|
return lines;
|
|
6567
7401
|
}
|
|
6568
7402
|
function parseLogFilters(options) {
|
|
@@ -6571,9 +7405,14 @@ function parseLogFilters(options) {
|
|
|
6571
7405
|
return null;
|
|
6572
7406
|
}
|
|
6573
7407
|
const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
|
|
6574
|
-
const
|
|
7408
|
+
const noProgress = options.progress === false;
|
|
7409
|
+
const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
|
|
6575
7410
|
if (slices && !selectsSingleLog) {
|
|
6576
|
-
writeError2("--tail, --grep, and --
|
|
7411
|
+
writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
|
|
7412
|
+
return null;
|
|
7413
|
+
}
|
|
7414
|
+
if (options.head !== void 0 && options.tail !== void 0) {
|
|
7415
|
+
writeError2("Use either --head or --tail, not both.");
|
|
6577
7416
|
return null;
|
|
6578
7417
|
}
|
|
6579
7418
|
if (options.context !== void 0 && options.grep === void 0) {
|
|
@@ -6582,6 +7421,8 @@ function parseLogFilters(options) {
|
|
|
6582
7421
|
}
|
|
6583
7422
|
const tail = parseOptionalCount(options.tail, "--tail");
|
|
6584
7423
|
if (tail === null) return null;
|
|
7424
|
+
const head = parseOptionalCount(options.head, "--head");
|
|
7425
|
+
if (head === null) return null;
|
|
6585
7426
|
const contextLines = parseOptionalCount(options.context, "--context");
|
|
6586
7427
|
if (contextLines === null) return null;
|
|
6587
7428
|
let grep;
|
|
@@ -6593,7 +7434,13 @@ function parseLogFilters(options) {
|
|
|
6593
7434
|
return null;
|
|
6594
7435
|
}
|
|
6595
7436
|
}
|
|
6596
|
-
return { tail, contextLines: contextLines ?? 0, grep };
|
|
7437
|
+
return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
|
|
7438
|
+
}
|
|
7439
|
+
function describeLog(log) {
|
|
7440
|
+
const kind = log.type ?? "";
|
|
7441
|
+
const where = log.parent ? ` in ${log.parent}` : "";
|
|
7442
|
+
const detail = `${kind}${where}`.trim();
|
|
7443
|
+
return detail ? `${log.step} [${detail}]` : String(log.step);
|
|
6597
7444
|
}
|
|
6598
7445
|
function chooseStepLog(logs, step, runId) {
|
|
6599
7446
|
const needle = step.toLowerCase();
|
|
@@ -6605,7 +7452,7 @@ function chooseStepLog(logs, step, runId) {
|
|
|
6605
7452
|
return null;
|
|
6606
7453
|
}
|
|
6607
7454
|
if (chosen.length > 1) {
|
|
6608
|
-
const candidates = chosen.map((l) => `${l.id} (${l
|
|
7455
|
+
const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
|
|
6609
7456
|
writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
|
|
6610
7457
|
return null;
|
|
6611
7458
|
}
|
|
@@ -6627,8 +7474,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
|
|
|
6627
7474
|
return chooseStepLog(allLogs, options.step, runId);
|
|
6628
7475
|
}
|
|
6629
7476
|
function printSingleLog(content, filters) {
|
|
6630
|
-
if (filters.grep !== void 0 || filters.tail !== void 0) {
|
|
6631
|
-
const lines = filterLogLines(content, filters
|
|
7477
|
+
if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
|
|
7478
|
+
const lines = filterLogLines(content, filters);
|
|
6632
7479
|
if (lines.length > 0) {
|
|
6633
7480
|
process.stdout.write(`${lines.join("\n")}
|
|
6634
7481
|
`);
|
|
@@ -6639,8 +7486,8 @@ function printSingleLog(content, filters) {
|
|
|
6639
7486
|
`);
|
|
6640
7487
|
}
|
|
6641
7488
|
function createPipelineLogsCommand() {
|
|
6642
|
-
const command = new
|
|
6643
|
-
command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
7489
|
+
const command = new Command14("logs");
|
|
7490
|
+
command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--head <n>", "with --log-id/--step, print only the first N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--no-progress", "with --log-id/--step, collapse progress-bar redraws (carriage-return or glued into one line) to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
6644
7491
|
validateOrgProjectPair(options);
|
|
6645
7492
|
const runId = parsePositiveId(runIdRaw);
|
|
6646
7493
|
if (runId === null) {
|
|
@@ -6679,7 +7526,9 @@ function createPipelineLogsCommand() {
|
|
|
6679
7526
|
String(l.id),
|
|
6680
7527
|
l.createdOn ?? "\u2014",
|
|
6681
7528
|
l.lineCount == null ? "" : `${l.lineCount} lines`,
|
|
6682
|
-
l.
|
|
7529
|
+
l.type ?? "",
|
|
7530
|
+
l.step ?? "",
|
|
7531
|
+
l.parent ? `(in ${l.parent})` : ""
|
|
6683
7532
|
]);
|
|
6684
7533
|
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
6685
7534
|
`);
|
|
@@ -6689,8 +7538,104 @@ function createPipelineLogsCommand() {
|
|
|
6689
7538
|
});
|
|
6690
7539
|
return command;
|
|
6691
7540
|
}
|
|
7541
|
+
function formatSize(bytes) {
|
|
7542
|
+
if (bytes === null) return "";
|
|
7543
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7544
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
7545
|
+
let value = bytes / 1024;
|
|
7546
|
+
let unit = 0;
|
|
7547
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7548
|
+
value /= 1024;
|
|
7549
|
+
unit++;
|
|
7550
|
+
}
|
|
7551
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
7552
|
+
}
|
|
7553
|
+
function createPipelineArtifactsCommand() {
|
|
7554
|
+
const command = new Command14("artifacts");
|
|
7555
|
+
command.description("List the build artifacts published by a pipeline run").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
|
|
7556
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
7557
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7558
|
+
if (options.json) {
|
|
7559
|
+
process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
|
|
7560
|
+
`);
|
|
7561
|
+
return;
|
|
7562
|
+
}
|
|
7563
|
+
if (artifacts.length === 0) {
|
|
7564
|
+
process.stdout.write(`No artifacts found for run ${runId}.
|
|
7565
|
+
`);
|
|
7566
|
+
return;
|
|
7567
|
+
}
|
|
7568
|
+
const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
|
|
7569
|
+
process.stdout.write(`${formatTable(rows)}
|
|
7570
|
+
`);
|
|
7571
|
+
})
|
|
7572
|
+
);
|
|
7573
|
+
return command;
|
|
7574
|
+
}
|
|
7575
|
+
function selectArtifacts(artifacts, name, all, runId) {
|
|
7576
|
+
const available = artifacts.map((a) => a.name).join(", ") || "(none)";
|
|
7577
|
+
if (all) return artifacts;
|
|
7578
|
+
if (name === void 0) {
|
|
7579
|
+
writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
|
|
7580
|
+
return null;
|
|
7581
|
+
}
|
|
7582
|
+
const match = artifacts.find((a) => a.name === name);
|
|
7583
|
+
if (!match) {
|
|
7584
|
+
writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
|
|
7585
|
+
return null;
|
|
7586
|
+
}
|
|
7587
|
+
return [match];
|
|
7588
|
+
}
|
|
7589
|
+
function createPipelineArtifactDownloadCommand() {
|
|
7590
|
+
const command = new Command14("artifact-download");
|
|
7591
|
+
command.description("Download a run's artifact (or --all) and extract it into a folder; no zip is left behind").argument("<run_id>", "pipeline run id").argument("[name]", "artifact name (see `pipeline artifacts`)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--path <dir>", "destination folder (default ./<name>; with --all, <dir>/<name> per artifact)").option("--all", "download every artifact of the run").option("--force", "overwrite files that already exist").option("--progress", "print byte-progress lines on stderr").option("--json", "output JSON").action(async (runIdRaw, name, options) => {
|
|
7592
|
+
validateOrgProjectPair(options);
|
|
7593
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7594
|
+
if (runId === null) {
|
|
7595
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7596
|
+
return;
|
|
7597
|
+
}
|
|
7598
|
+
if (options.all && name !== void 0) {
|
|
7599
|
+
writeError2("Use either an artifact name or --all, not both.");
|
|
7600
|
+
return;
|
|
7601
|
+
}
|
|
7602
|
+
let context;
|
|
7603
|
+
try {
|
|
7604
|
+
const resolved = await resolvePipelineContext(options);
|
|
7605
|
+
context = resolved.context;
|
|
7606
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7607
|
+
const selected = selectArtifacts(artifacts, name, options.all === true, runId);
|
|
7608
|
+
if (selected === null) return;
|
|
7609
|
+
const results = await Promise.all(
|
|
7610
|
+
selected.map(async (artifact) => {
|
|
7611
|
+
const destination = path3.resolve(
|
|
7612
|
+
options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
|
|
7613
|
+
);
|
|
7614
|
+
const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
|
|
7615
|
+
`) : void 0;
|
|
7616
|
+
const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
|
|
7617
|
+
const files = extractArtifactZip(zip, destination, {
|
|
7618
|
+
artifactName: artifact.name,
|
|
7619
|
+
force: options.force === true
|
|
7620
|
+
});
|
|
7621
|
+
return { name: artifact.name, path: destination, files: files.length };
|
|
7622
|
+
})
|
|
7623
|
+
);
|
|
7624
|
+
if (options.json) {
|
|
7625
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
7626
|
+
`);
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
process.stdout.write(`${results.map((r) => r.path).join("\n")}
|
|
7630
|
+
`);
|
|
7631
|
+
} catch (err) {
|
|
7632
|
+
handlePipelineError(err, context);
|
|
7633
|
+
}
|
|
7634
|
+
});
|
|
7635
|
+
return command;
|
|
7636
|
+
}
|
|
6692
7637
|
function createPipelineTestsCommand() {
|
|
6693
|
-
const command = new
|
|
7638
|
+
const command = new Command14("tests");
|
|
6694
7639
|
command.description("Show a run's test results: summary plus failing tests by name (no log grepping needed)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--failed", "list only the failing tests").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
6695
7640
|
validateOrgProjectPair(options);
|
|
6696
7641
|
const runId = parsePositiveId(runIdRaw);
|
|
@@ -6747,7 +7692,7 @@ function collectParameter(value, previous) {
|
|
|
6747
7692
|
return previous.concat([value]);
|
|
6748
7693
|
}
|
|
6749
7694
|
function createPipelineStartCommand() {
|
|
6750
|
-
const command = new
|
|
7695
|
+
const command = new Command14("start");
|
|
6751
7696
|
command.description("Queue a new run of a pipeline definition").argument("<def_id>", "pipeline definition id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--branch <branch>", "branch to run against (default: pipeline default branch)").option("--parameter <key=value>", "template parameter (repeatable)", collectParameter, []).option("--json", "output JSON").action(async (defIdRaw, options) => {
|
|
6752
7697
|
validateOrgProjectPair(options);
|
|
6753
7698
|
const defId = parsePositiveId(defIdRaw);
|
|
@@ -6783,20 +7728,42 @@ ${result.webUrl ?? "\u2014"}
|
|
|
6783
7728
|
return command;
|
|
6784
7729
|
}
|
|
6785
7730
|
function createPipelineCommand() {
|
|
6786
|
-
const command = new
|
|
7731
|
+
const command = new Command14("pipeline");
|
|
6787
7732
|
command.description("Manage Azure DevOps pipelines");
|
|
6788
7733
|
command.addCommand(createPipelineListCommand());
|
|
6789
7734
|
command.addCommand(createPipelineGetRunsCommand());
|
|
6790
7735
|
command.addCommand(createPipelineWaitCommand());
|
|
6791
7736
|
command.addCommand(createPipelineGetRunDetailCommand());
|
|
6792
7737
|
command.addCommand(createPipelineLogsCommand());
|
|
7738
|
+
command.addCommand(createPipelineArtifactsCommand());
|
|
7739
|
+
command.addCommand(createPipelineArtifactDownloadCommand());
|
|
6793
7740
|
command.addCommand(createPipelineTestsCommand());
|
|
6794
7741
|
command.addCommand(createPipelineStartCommand());
|
|
6795
7742
|
return command;
|
|
6796
7743
|
}
|
|
6797
7744
|
|
|
6798
7745
|
// src/commands/comments.ts
|
|
6799
|
-
import { Command as
|
|
7746
|
+
import { Command as Command15 } from "commander";
|
|
7747
|
+
async function runCommentWrite(options, target, request, humanLine) {
|
|
7748
|
+
let context;
|
|
7749
|
+
try {
|
|
7750
|
+
context = resolveContext(options);
|
|
7751
|
+
const credential = await requireAuthCredential(context.org);
|
|
7752
|
+
const result = await request(context, credential);
|
|
7753
|
+
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}
|
|
7754
|
+
` : `${humanLine(result)}
|
|
7755
|
+
`);
|
|
7756
|
+
} catch (err) {
|
|
7757
|
+
if (target.commentId === void 0) {
|
|
7758
|
+
handleCommandError(err, target.id, context, "write");
|
|
7759
|
+
} else {
|
|
7760
|
+
handleCommentWriteError(err, target.id, target.commentId, context);
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
function commentFormat(options) {
|
|
7765
|
+
return options.markdown === true ? "markdown" : "html";
|
|
7766
|
+
}
|
|
6800
7767
|
function writeError3(message) {
|
|
6801
7768
|
process.stderr.write(`Error: ${message}
|
|
6802
7769
|
`);
|
|
@@ -6816,7 +7783,7 @@ function formatComments(result, convertMarkdown) {
|
|
|
6816
7783
|
return lines.join("\n");
|
|
6817
7784
|
}
|
|
6818
7785
|
function createCommentsListCommand() {
|
|
6819
|
-
const command = new
|
|
7786
|
+
const command = new Command15("list");
|
|
6820
7787
|
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "convert HTML comment bodies to markdown").action(async (idStr, options) => {
|
|
6821
7788
|
validateOrgProjectPair(options);
|
|
6822
7789
|
const id = parseWorkItemId(idStr);
|
|
@@ -6844,47 +7811,101 @@ function createCommentsListCommand() {
|
|
|
6844
7811
|
return command;
|
|
6845
7812
|
}
|
|
6846
7813
|
function createCommentsAddCommand() {
|
|
6847
|
-
const command = new
|
|
7814
|
+
const command = new Command15("add");
|
|
6848
7815
|
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, text, options) => {
|
|
6849
7816
|
validateOrgProjectPair(options);
|
|
6850
7817
|
const id = parseWorkItemId(idStr);
|
|
6851
7818
|
if (text.trim() === "") {
|
|
6852
7819
|
writeError3("Comment text must be a non-empty string.");
|
|
6853
7820
|
}
|
|
6854
|
-
|
|
7821
|
+
await runCommentWrite(
|
|
7822
|
+
options,
|
|
7823
|
+
{ id },
|
|
7824
|
+
(context, credential) => addWorkItemComment(context, id, credential, text, commentFormat(options)),
|
|
7825
|
+
(result) => `Added comment #${result.commentId} to work item #${result.workItemId}`
|
|
7826
|
+
);
|
|
7827
|
+
});
|
|
7828
|
+
return command;
|
|
7829
|
+
}
|
|
7830
|
+
function parseCommentId(value) {
|
|
7831
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) {
|
|
7832
|
+
writeError3(`Invalid comment ID "${value}". Expected a positive integer.`);
|
|
7833
|
+
}
|
|
7834
|
+
return Number(value);
|
|
7835
|
+
}
|
|
7836
|
+
function resolveEditText(inline, file) {
|
|
7837
|
+
if (inline !== void 0 && file !== void 0) {
|
|
7838
|
+
writeError3("Cannot specify both inline text and --file.");
|
|
7839
|
+
}
|
|
7840
|
+
let body = inline;
|
|
7841
|
+
if (file !== void 0) {
|
|
6855
7842
|
try {
|
|
6856
|
-
|
|
6857
|
-
const credential = await requireAuthCredential(context.org);
|
|
6858
|
-
const format = options.markdown === true ? "markdown" : "html";
|
|
6859
|
-
const result = await addWorkItemComment(context, id, credential, text, format);
|
|
6860
|
-
if (options.json) {
|
|
6861
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6862
|
-
`);
|
|
6863
|
-
return;
|
|
6864
|
-
}
|
|
6865
|
-
process.stdout.write(`Added comment #${result.commentId} to work item #${result.workItemId}
|
|
6866
|
-
`);
|
|
7843
|
+
body = readTextSource(file);
|
|
6867
7844
|
} catch (err) {
|
|
6868
|
-
|
|
7845
|
+
writeError3(err.message);
|
|
6869
7846
|
}
|
|
7847
|
+
}
|
|
7848
|
+
if (body === void 0 || body.trim() === "") {
|
|
7849
|
+
writeError3("Comment text must be a non-empty string. Pass the text inline or use --file <path>.");
|
|
7850
|
+
}
|
|
7851
|
+
return body;
|
|
7852
|
+
}
|
|
7853
|
+
function handleCommentWriteError(err, id, commentId, context) {
|
|
7854
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
7855
|
+
writeError3(
|
|
7856
|
+
`Comment ${commentId} not found on work item ${id} in ${context?.org}/${context?.project}.`
|
|
7857
|
+
);
|
|
7858
|
+
}
|
|
7859
|
+
handleCommandError(err, id, context, "write");
|
|
7860
|
+
}
|
|
7861
|
+
function createCommentsEditCommand() {
|
|
7862
|
+
const command = new Command15("edit");
|
|
7863
|
+
command.description("Edit an existing comment on a work item").argument("<id>", "work item ID").argument("<commentId>", "comment ID").argument("[text]", "new comment text (or use --file)").option("--file <path>", 'read the new comment text from a file ("-" for stdin)').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, commentIdStr, text, options) => {
|
|
7864
|
+
validateOrgProjectPair(options);
|
|
7865
|
+
const id = parseWorkItemId(idStr);
|
|
7866
|
+
const commentId = parseCommentId(commentIdStr);
|
|
7867
|
+
const body = resolveEditText(text, options.file);
|
|
7868
|
+
await runCommentWrite(
|
|
7869
|
+
options,
|
|
7870
|
+
{ id, commentId },
|
|
7871
|
+
(context, credential) => updateWorkItemComment(context, id, commentId, credential, body, commentFormat(options)),
|
|
7872
|
+
(result) => `Updated comment #${result.commentId} on work item #${result.workItemId}`
|
|
7873
|
+
);
|
|
7874
|
+
});
|
|
7875
|
+
return command;
|
|
7876
|
+
}
|
|
7877
|
+
function createCommentsDeleteCommand() {
|
|
7878
|
+
const command = new Command15("delete");
|
|
7879
|
+
command.description("Delete a comment from a work item (no confirmation prompt)").argument("<id>", "work item ID").argument("<commentId>", "comment ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, commentIdStr, options) => {
|
|
7880
|
+
validateOrgProjectPair(options);
|
|
7881
|
+
const id = parseWorkItemId(idStr);
|
|
7882
|
+
const commentId = parseCommentId(commentIdStr);
|
|
7883
|
+
await runCommentWrite(
|
|
7884
|
+
options,
|
|
7885
|
+
{ id, commentId },
|
|
7886
|
+
(context, credential) => deleteWorkItemComment(context, id, commentId, credential),
|
|
7887
|
+
(result) => `Deleted comment #${result.commentId} from work item #${result.workItemId}`
|
|
7888
|
+
);
|
|
6870
7889
|
});
|
|
6871
7890
|
return command;
|
|
6872
7891
|
}
|
|
6873
7892
|
function createCommentsCommand() {
|
|
6874
|
-
const command = new
|
|
7893
|
+
const command = new Command15("comments");
|
|
6875
7894
|
command.description("Manage Azure DevOps work item comments");
|
|
6876
7895
|
command.addCommand(createCommentsListCommand());
|
|
6877
7896
|
command.addCommand(createCommentsAddCommand());
|
|
7897
|
+
command.addCommand(createCommentsEditCommand());
|
|
7898
|
+
command.addCommand(createCommentsDeleteCommand());
|
|
6878
7899
|
return command;
|
|
6879
7900
|
}
|
|
6880
7901
|
|
|
6881
7902
|
// src/commands/download-attachment.ts
|
|
6882
|
-
import { Command as
|
|
7903
|
+
import { Command as Command16 } from "commander";
|
|
6883
7904
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
6884
|
-
import { existsSync as
|
|
7905
|
+
import { existsSync as existsSync7 } from "fs";
|
|
6885
7906
|
import { join as join3 } from "path";
|
|
6886
7907
|
function createDownloadAttachmentCommand() {
|
|
6887
|
-
const command = new
|
|
7908
|
+
const command = new Command16("download-attachment");
|
|
6888
7909
|
command.description("Download an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to download").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--output <dir>", "target directory for the downloaded file").action(
|
|
6889
7910
|
async (idStr, filename, options) => {
|
|
6890
7911
|
const id = parseWorkItemId(idStr);
|
|
@@ -6894,7 +7915,7 @@ function createDownloadAttachmentCommand() {
|
|
|
6894
7915
|
context = resolveContext(options);
|
|
6895
7916
|
const credential = await requireAuthCredential(context.org);
|
|
6896
7917
|
const outputDir = options.output ?? ".";
|
|
6897
|
-
if (!
|
|
7918
|
+
if (!existsSync7(outputDir)) {
|
|
6898
7919
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
6899
7920
|
`);
|
|
6900
7921
|
process.exit(1);
|
|
@@ -6926,17 +7947,17 @@ function createDownloadAttachmentCommand() {
|
|
|
6926
7947
|
}
|
|
6927
7948
|
|
|
6928
7949
|
// src/commands/add-attachment.ts
|
|
6929
|
-
import { Command as
|
|
6930
|
-
import { existsSync as
|
|
7950
|
+
import { Command as Command17 } from "commander";
|
|
7951
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
6931
7952
|
import { readFile } from "fs/promises";
|
|
6932
7953
|
import { basename } from "path";
|
|
6933
7954
|
function createAddAttachmentCommand() {
|
|
6934
|
-
const command = new
|
|
7955
|
+
const command = new Command17("add-attachment");
|
|
6935
7956
|
command.description("Attach a local file to an Azure DevOps work item").argument("<id>", "work item ID").argument("<file>", "path to the local file to upload").option("--comment <text>", "optional comment to store with the attachment").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
|
|
6936
7957
|
async (idStr, file, options) => {
|
|
6937
7958
|
const id = parseWorkItemId(idStr);
|
|
6938
7959
|
validateOrgProjectPair(options);
|
|
6939
|
-
if (!
|
|
7960
|
+
if (!existsSync8(file)) {
|
|
6940
7961
|
process.stderr.write(`Error: File not found: ${file}
|
|
6941
7962
|
`);
|
|
6942
7963
|
process.exit(1);
|
|
@@ -6978,12 +7999,12 @@ function createAddAttachmentCommand() {
|
|
|
6978
7999
|
}
|
|
6979
8000
|
|
|
6980
8001
|
// src/commands/delete-attachment.ts
|
|
6981
|
-
import { Command as
|
|
8002
|
+
import { Command as Command18 } from "commander";
|
|
6982
8003
|
function formatUploadDate(iso) {
|
|
6983
8004
|
return iso ? iso.slice(0, 10) : "unknown date";
|
|
6984
8005
|
}
|
|
6985
8006
|
function createDeleteAttachmentCommand() {
|
|
6986
|
-
const command = new
|
|
8007
|
+
const command = new Command18("delete-attachment");
|
|
6987
8008
|
command.description("Remove an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to remove").option("--id <attachmentId>", "attachment GUID, to disambiguate when the filename is shared by more than one attachment").option("-y, --yes", "skip the interactive confirmation prompt").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
|
|
6988
8009
|
async (idStr, filename, options) => {
|
|
6989
8010
|
const id = parseWorkItemId(idStr);
|
|
@@ -7060,7 +8081,7 @@ function createDeleteAttachmentCommand() {
|
|
|
7060
8081
|
}
|
|
7061
8082
|
|
|
7062
8083
|
// src/commands/relations.ts
|
|
7063
|
-
import { Command as
|
|
8084
|
+
import { Command as Command19 } from "commander";
|
|
7064
8085
|
|
|
7065
8086
|
// src/services/relations-client.ts
|
|
7066
8087
|
var API_VERSION2 = "7.1";
|
|
@@ -7277,7 +8298,7 @@ function parsePositiveInt(value, label) {
|
|
|
7277
8298
|
return n;
|
|
7278
8299
|
}
|
|
7279
8300
|
function createRelationsCommand() {
|
|
7280
|
-
const relations = new
|
|
8301
|
+
const relations = new Command19("relations").description("Manage work item relations");
|
|
7281
8302
|
addCommonOptions(
|
|
7282
8303
|
relations.command("types").description("List all available work item relation types")
|
|
7283
8304
|
).action(async (opts) => {
|
|
@@ -7356,7 +8377,7 @@ function createRelationsCommand() {
|
|
|
7356
8377
|
|
|
7357
8378
|
// src/services/update-check.ts
|
|
7358
8379
|
import fs2 from "fs";
|
|
7359
|
-
import
|
|
8380
|
+
import path4 from "path";
|
|
7360
8381
|
import os from "os";
|
|
7361
8382
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
7362
8383
|
var FETCH_TIMEOUT_MS = 1500;
|
|
@@ -7374,7 +8395,7 @@ function skipsUpdateCheck(commandPath) {
|
|
|
7374
8395
|
return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
|
|
7375
8396
|
}
|
|
7376
8397
|
function getCachePath() {
|
|
7377
|
-
return
|
|
8398
|
+
return path4.join(os.homedir(), ".azdo", "update-check.json");
|
|
7378
8399
|
}
|
|
7379
8400
|
function defaultReadCache() {
|
|
7380
8401
|
try {
|
|
@@ -7386,7 +8407,7 @@ function defaultReadCache() {
|
|
|
7386
8407
|
function defaultWriteCache(data) {
|
|
7387
8408
|
try {
|
|
7388
8409
|
const cachePath = getCachePath();
|
|
7389
|
-
fs2.mkdirSync(
|
|
8410
|
+
fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
|
|
7390
8411
|
fs2.writeFileSync(cachePath, data);
|
|
7391
8412
|
} catch {
|
|
7392
8413
|
}
|
|
@@ -7472,11 +8493,12 @@ async function getUpdateNotice(opts) {
|
|
|
7472
8493
|
|
|
7473
8494
|
// src/program.ts
|
|
7474
8495
|
function createProgram() {
|
|
7475
|
-
const program2 = new
|
|
8496
|
+
const program2 = new Command20();
|
|
7476
8497
|
program2.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
7477
8498
|
program2.option("--no-update-check", "Skip the check for a newer published version");
|
|
7478
8499
|
program2.option("--trace <filepath>", "Append redacted HTTP request/response trace to a file (owner-read-only permissions)");
|
|
7479
8500
|
program2.addCommand(createGetItemCommand());
|
|
8501
|
+
program2.addCommand(createListItemsCommand());
|
|
7480
8502
|
program2.addCommand(createAuthCommand());
|
|
7481
8503
|
program2.addCommand(createClearPatCommand());
|
|
7482
8504
|
program2.addCommand(createConfigCommand());
|