azdo-cli 0.10.0-develop.467 → 0.10.0-develop.490
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.js +355 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ azdo pr comments # active-branch PR
|
|
|
58
58
|
azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
|
|
59
59
|
azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
|
|
60
60
|
azdo pr comments --code-related-only # only file/line-anchored threads
|
|
61
|
-
azdo pr status # PR checks (status + branch policies) + code-comment counts
|
|
61
|
+
azdo pr status # PR checks (status + branch policies + pipeline builds) + code-comment counts
|
|
62
62
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
63
63
|
azdo pr comment-reopen 17 --pr-number 64
|
|
64
64
|
|
|
@@ -68,6 +68,14 @@ azdo pipeline get-runs 12 --branch develop --limit 1
|
|
|
68
68
|
azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
|
|
69
69
|
azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
|
|
70
70
|
azdo pipeline start 12 --branch develop --parameter env=staging
|
|
71
|
+
|
|
72
|
+
# Work item relations — types, add, remove, list
|
|
73
|
+
azdo relations types # list all relation types (Child, Parent, Related, ...)
|
|
74
|
+
azdo relations types --json # machine-readable JSON array
|
|
75
|
+
azdo relations add child 1000 2000 # make #2000 a child of #1000 (idempotent)
|
|
76
|
+
azdo relations remove child 1000 2000 # remove the child relation
|
|
77
|
+
azdo relations list 1000 # show all relations on work item #1000
|
|
78
|
+
azdo relations list 1000 --json # JSON: { workItemId, relations: [...] }
|
|
71
79
|
```
|
|
72
80
|
|
|
73
81
|
## Documentation
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
} from "./chunk-XVXMDWQE.js";
|
|
39
39
|
|
|
40
40
|
// src/index.ts
|
|
41
|
-
import { Command as
|
|
41
|
+
import { Command as Command17 } from "commander";
|
|
42
42
|
|
|
43
43
|
// src/version.ts
|
|
44
44
|
import { readFileSync } from "fs";
|
|
@@ -2907,6 +2907,16 @@ function buildPolicyEvaluationsUrl(context, projectId, prId) {
|
|
|
2907
2907
|
url.searchParams.set("artifactId", `vstfs:///CodeReview/CodeReviewId/${projectId}/${prId}`);
|
|
2908
2908
|
return url;
|
|
2909
2909
|
}
|
|
2910
|
+
function buildPullRequestBuildsUrl(context, prId) {
|
|
2911
|
+
const url = new URL(
|
|
2912
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/build/builds`
|
|
2913
|
+
);
|
|
2914
|
+
url.searchParams.set("branchName", `refs/pull/${prId}/merge`);
|
|
2915
|
+
url.searchParams.set("queryOrder", "queueTimeDescending");
|
|
2916
|
+
url.searchParams.set("$top", "50");
|
|
2917
|
+
url.searchParams.set("api-version", "7.1");
|
|
2918
|
+
return url;
|
|
2919
|
+
}
|
|
2910
2920
|
function mapPullRequest(repo, pullRequest) {
|
|
2911
2921
|
return {
|
|
2912
2922
|
id: pullRequest.pullRequestId,
|
|
@@ -2966,6 +2976,22 @@ function mapPolicyEvaluationState(status2) {
|
|
|
2966
2976
|
return status2;
|
|
2967
2977
|
}
|
|
2968
2978
|
}
|
|
2979
|
+
function mapBuildToCheckState(build) {
|
|
2980
|
+
if (build.status !== "completed") {
|
|
2981
|
+
return "pending";
|
|
2982
|
+
}
|
|
2983
|
+
switch (build.result) {
|
|
2984
|
+
case "succeeded":
|
|
2985
|
+
case "partiallySucceeded":
|
|
2986
|
+
return "succeeded";
|
|
2987
|
+
case "failed":
|
|
2988
|
+
return "failed";
|
|
2989
|
+
case "canceled":
|
|
2990
|
+
return "error";
|
|
2991
|
+
default:
|
|
2992
|
+
return "pending";
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2969
2995
|
function mapPolicyEvaluationName(evaluation) {
|
|
2970
2996
|
const display = evaluation.configuration?.settings?.displayName?.trim() || evaluation.configuration?.type?.displayName?.trim();
|
|
2971
2997
|
if (display) {
|
|
@@ -2987,7 +3013,8 @@ function mapPolicyEvaluationCheck(evaluation) {
|
|
|
2987
3013
|
createdBy: null,
|
|
2988
3014
|
createdAt: null,
|
|
2989
3015
|
updatedAt: null,
|
|
2990
|
-
source: "policy"
|
|
3016
|
+
source: "policy",
|
|
3017
|
+
isBlocking: evaluation.configuration?.isBlocking ?? null
|
|
2991
3018
|
};
|
|
2992
3019
|
}
|
|
2993
3020
|
function mapComment(comment) {
|
|
@@ -3009,7 +3036,7 @@ function mapThread(thread) {
|
|
|
3009
3036
|
}
|
|
3010
3037
|
return {
|
|
3011
3038
|
id: thread.id,
|
|
3012
|
-
status: thread.status,
|
|
3039
|
+
status: thread.status ?? "unknown",
|
|
3013
3040
|
threadContext: thread.threadContext?.filePath ?? null,
|
|
3014
3041
|
comments
|
|
3015
3042
|
};
|
|
@@ -3017,7 +3044,7 @@ function mapThread(thread) {
|
|
|
3017
3044
|
function toActiveCommentThread(thread) {
|
|
3018
3045
|
return {
|
|
3019
3046
|
id: thread.id,
|
|
3020
|
-
status: thread.status,
|
|
3047
|
+
status: thread.status ?? "unknown",
|
|
3021
3048
|
threadContext: thread.threadContext?.filePath ?? null,
|
|
3022
3049
|
comments: thread.comments.map(mapComment).filter((comment) => comment !== null)
|
|
3023
3050
|
};
|
|
@@ -3088,6 +3115,24 @@ async function getPullRequestPolicyEvaluations(context, cred, projectId, prId) {
|
|
|
3088
3115
|
const data = await readJsonResponse(response);
|
|
3089
3116
|
return data.value.map(mapPolicyEvaluationCheck).filter((check) => check !== null);
|
|
3090
3117
|
}
|
|
3118
|
+
async function getPullRequestBuilds(context, cred, prId) {
|
|
3119
|
+
const response = await fetchWithErrors(buildPullRequestBuildsUrl(context, prId).toString(), {
|
|
3120
|
+
headers: authHeaders(cred)
|
|
3121
|
+
});
|
|
3122
|
+
const data = await readJsonResponse(response);
|
|
3123
|
+
return data.value.map((build) => ({
|
|
3124
|
+
id: build.id,
|
|
3125
|
+
state: mapBuildToCheckState(build),
|
|
3126
|
+
name: build.definition?.name ?? `Build #${build.id}`,
|
|
3127
|
+
description: null,
|
|
3128
|
+
targetUrl: build._links?.web?.href ?? null,
|
|
3129
|
+
createdBy: null,
|
|
3130
|
+
createdAt: build.queueTime ?? null,
|
|
3131
|
+
updatedAt: build.finishTime ?? null,
|
|
3132
|
+
source: "build",
|
|
3133
|
+
isBlocking: null
|
|
3134
|
+
}));
|
|
3135
|
+
}
|
|
3091
3136
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
3092
3137
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
3093
3138
|
status: "active",
|
|
@@ -3205,7 +3250,8 @@ function formatPullRequestChecks(checks, checksError) {
|
|
|
3205
3250
|
}
|
|
3206
3251
|
const lines = ["Checks:"];
|
|
3207
3252
|
for (const check of checks) {
|
|
3208
|
-
|
|
3253
|
+
const optionalTag = check.isBlocking === false ? " [optional]" : "";
|
|
3254
|
+
lines.push(`- [${check.state}] ${check.name}${optionalTag}`);
|
|
3209
3255
|
if ((check.state === "failed" || check.state === "error") && check.description) {
|
|
3210
3256
|
lines.push(` Detail: ${check.description}`);
|
|
3211
3257
|
}
|
|
@@ -3258,6 +3304,13 @@ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, pro
|
|
|
3258
3304
|
policyOk = false;
|
|
3259
3305
|
}
|
|
3260
3306
|
}
|
|
3307
|
+
let buildChecks = [];
|
|
3308
|
+
let buildsOk = true;
|
|
3309
|
+
try {
|
|
3310
|
+
buildChecks = await getPullRequestBuilds(context, cred, pullRequest.id);
|
|
3311
|
+
} catch {
|
|
3312
|
+
buildsOk = false;
|
|
3313
|
+
}
|
|
3261
3314
|
let codeCommentCounts;
|
|
3262
3315
|
try {
|
|
3263
3316
|
const threads = await getPullRequestThreads(context, repo, cred, pullRequest.id);
|
|
@@ -3265,8 +3318,8 @@ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, pro
|
|
|
3265
3318
|
} catch {
|
|
3266
3319
|
codeCommentCounts = { open: 0, closed: 0 };
|
|
3267
3320
|
}
|
|
3268
|
-
const checks = [...statusChecks, ...policyChecks];
|
|
3269
|
-
const checksError = checks.length === 0 && (!statusOk || !policyOk) ? "Azure DevOps request failed" : null;
|
|
3321
|
+
const checks = [...statusChecks, ...policyChecks, ...buildChecks];
|
|
3322
|
+
const checksError = checks.length === 0 && (!statusOk || !policyOk || !buildsOk) ? "Azure DevOps request failed" : null;
|
|
3270
3323
|
return {
|
|
3271
3324
|
...pullRequest,
|
|
3272
3325
|
checks,
|
|
@@ -4650,6 +4703,299 @@ function createDownloadAttachmentCommand() {
|
|
|
4650
4703
|
return command;
|
|
4651
4704
|
}
|
|
4652
4705
|
|
|
4706
|
+
// src/commands/relations.ts
|
|
4707
|
+
import { Command as Command16 } from "commander";
|
|
4708
|
+
|
|
4709
|
+
// src/services/relations-client.ts
|
|
4710
|
+
var API_VERSION2 = "7.1";
|
|
4711
|
+
function buildRelationTypesUrl(context) {
|
|
4712
|
+
const url = new URL(
|
|
4713
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/workitemrelationtypes`
|
|
4714
|
+
);
|
|
4715
|
+
url.searchParams.set("api-version", API_VERSION2);
|
|
4716
|
+
return url;
|
|
4717
|
+
}
|
|
4718
|
+
function buildWorkItemUrl2(context, id, expand) {
|
|
4719
|
+
const url = new URL(
|
|
4720
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
4721
|
+
);
|
|
4722
|
+
url.searchParams.set("api-version", API_VERSION2);
|
|
4723
|
+
if (expand) url.searchParams.set("$expand", expand);
|
|
4724
|
+
return url;
|
|
4725
|
+
}
|
|
4726
|
+
function buildBatchWorkItemsUrl(context, ids) {
|
|
4727
|
+
const url = new URL(
|
|
4728
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems`
|
|
4729
|
+
);
|
|
4730
|
+
url.searchParams.set("ids", ids.join(","));
|
|
4731
|
+
url.searchParams.set("fields", "System.Id,System.Title");
|
|
4732
|
+
url.searchParams.set("api-version", API_VERSION2);
|
|
4733
|
+
return url;
|
|
4734
|
+
}
|
|
4735
|
+
function mapRelationType(raw) {
|
|
4736
|
+
return {
|
|
4737
|
+
referenceName: raw.referenceName,
|
|
4738
|
+
name: raw.name,
|
|
4739
|
+
usage: raw.attributes?.usage ?? "unknown",
|
|
4740
|
+
enabled: raw.attributes?.enabled !== false,
|
|
4741
|
+
directional: raw.attributes?.directional ?? null
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
async function readJsonResponse3(response) {
|
|
4745
|
+
if (!response.ok) throw new Error(`HTTP_${response.status}`);
|
|
4746
|
+
return await response.json();
|
|
4747
|
+
}
|
|
4748
|
+
function parseTargetId(url) {
|
|
4749
|
+
const match = /\/workItems\/(\d+)$/i.exec(url);
|
|
4750
|
+
return match ? Number(match[1]) : null;
|
|
4751
|
+
}
|
|
4752
|
+
async function getWorkItemRelationTypes(context, cred) {
|
|
4753
|
+
const url = buildRelationTypesUrl(context);
|
|
4754
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
4755
|
+
headers: authHeaders(cred)
|
|
4756
|
+
});
|
|
4757
|
+
const body = await readJsonResponse3(response);
|
|
4758
|
+
return (body.value ?? []).map(mapRelationType).filter((t) => t.usage === "workItemLink" && t.enabled);
|
|
4759
|
+
}
|
|
4760
|
+
async function resolveRelationType(context, cred, alias) {
|
|
4761
|
+
const types = await getWorkItemRelationTypes(context, cred);
|
|
4762
|
+
const lower = alias.toLowerCase();
|
|
4763
|
+
const match = types.find((t) => t.name.toLowerCase() === lower);
|
|
4764
|
+
if (!match) throw new Error(`UNKNOWN_RELATION_TYPE:${alias}`);
|
|
4765
|
+
return match;
|
|
4766
|
+
}
|
|
4767
|
+
async function getWorkItemWithRelations(context, cred, id) {
|
|
4768
|
+
const url = buildWorkItemUrl2(context, id, "relations");
|
|
4769
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
4770
|
+
headers: authHeaders(cred)
|
|
4771
|
+
});
|
|
4772
|
+
return readJsonResponse3(response);
|
|
4773
|
+
}
|
|
4774
|
+
async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
4775
|
+
if (id1 === id2) throw new Error("SELF_RELATION");
|
|
4776
|
+
const relType = await resolveRelationType(context, cred, type);
|
|
4777
|
+
const workItem = await getWorkItemWithRelations(context, cred, id1);
|
|
4778
|
+
const targetUrl = `https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/workItems/${id2}`;
|
|
4779
|
+
const existing = (workItem.relations ?? []).find(
|
|
4780
|
+
(r) => r.rel === relType.referenceName && r.url.toLowerCase() === targetUrl.toLowerCase()
|
|
4781
|
+
);
|
|
4782
|
+
if (existing) {
|
|
4783
|
+
return { status: "already_exists", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
4784
|
+
}
|
|
4785
|
+
const patchUrl = buildWorkItemUrl2(context, id1);
|
|
4786
|
+
const response = await fetchWithErrors(patchUrl.toString(), {
|
|
4787
|
+
method: "PATCH",
|
|
4788
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
4789
|
+
body: JSON.stringify([
|
|
4790
|
+
{ op: "add", path: "/relations/-", value: { rel: relType.referenceName, url: targetUrl } }
|
|
4791
|
+
])
|
|
4792
|
+
});
|
|
4793
|
+
if (!response.ok) throw new Error(`HTTP_${response.status}`);
|
|
4794
|
+
return { status: "added", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
4795
|
+
}
|
|
4796
|
+
async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
4797
|
+
if (id1 === id2) throw new Error("SELF_RELATION");
|
|
4798
|
+
const relType = await resolveRelationType(context, cred, type);
|
|
4799
|
+
const workItem = await getWorkItemWithRelations(context, cred, id1);
|
|
4800
|
+
const relations = workItem.relations ?? [];
|
|
4801
|
+
const index = relations.findIndex(
|
|
4802
|
+
(r) => r.rel === relType.referenceName && parseTargetId(r.url) === id2
|
|
4803
|
+
);
|
|
4804
|
+
if (index === -1) {
|
|
4805
|
+
return { status: "not_found", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
4806
|
+
}
|
|
4807
|
+
const patchUrl = buildWorkItemUrl2(context, id1);
|
|
4808
|
+
const response = await fetchWithErrors(patchUrl.toString(), {
|
|
4809
|
+
method: "PATCH",
|
|
4810
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
4811
|
+
body: JSON.stringify([{ op: "remove", path: `/relations/${index}` }])
|
|
4812
|
+
});
|
|
4813
|
+
if (!response.ok) throw new Error(`HTTP_${response.status}`);
|
|
4814
|
+
return { status: "removed", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
4815
|
+
}
|
|
4816
|
+
async function listWorkItemRelations(context, cred, id) {
|
|
4817
|
+
const workItem = await getWorkItemWithRelations(context, cred, id);
|
|
4818
|
+
const allRelations = workItem.relations ?? [];
|
|
4819
|
+
const wiRelations = allRelations.filter(
|
|
4820
|
+
(r) => !r.rel.startsWith("AttachedFile") && !r.rel.startsWith("Hyperlink") && !r.rel.startsWith("ArtifactLink")
|
|
4821
|
+
);
|
|
4822
|
+
if (wiRelations.length === 0) {
|
|
4823
|
+
return { workItemId: id, relations: [] };
|
|
4824
|
+
}
|
|
4825
|
+
const types = await getWorkItemRelationTypes(context, cred);
|
|
4826
|
+
const typeNameMap = new Map(types.map((t) => [t.referenceName, t.name]));
|
|
4827
|
+
const targetIds = wiRelations.map((r) => parseTargetId(r.url)).filter((n) => n !== null);
|
|
4828
|
+
const titleMap = /* @__PURE__ */ new Map();
|
|
4829
|
+
if (targetIds.length > 0) {
|
|
4830
|
+
try {
|
|
4831
|
+
const batchUrl = buildBatchWorkItemsUrl(context, targetIds);
|
|
4832
|
+
const batchResponse = await fetchWithErrors(batchUrl.toString(), {
|
|
4833
|
+
headers: authHeaders(cred)
|
|
4834
|
+
});
|
|
4835
|
+
const batchBody = await readJsonResponse3(batchResponse);
|
|
4836
|
+
for (const item of batchBody.value ?? []) {
|
|
4837
|
+
titleMap.set(item.id, item.fields["System.Title"] ?? "");
|
|
4838
|
+
}
|
|
4839
|
+
} catch {
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
const relations = wiRelations.map((r) => {
|
|
4843
|
+
const targetId = parseTargetId(r.url);
|
|
4844
|
+
return {
|
|
4845
|
+
rel: r.rel,
|
|
4846
|
+
relName: typeNameMap.get(r.rel) ?? r.rel,
|
|
4847
|
+
targetId: targetId ?? 0,
|
|
4848
|
+
targetTitle: targetId !== null ? titleMap.get(targetId) ?? null : null,
|
|
4849
|
+
targetUrl: r.url,
|
|
4850
|
+
comment: r.attributes?.comment ?? null
|
|
4851
|
+
};
|
|
4852
|
+
});
|
|
4853
|
+
return { workItemId: id, relations };
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
// src/commands/relations.ts
|
|
4857
|
+
function addCommonOptions(cmd) {
|
|
4858
|
+
return cmd.option("--json", "Output as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project");
|
|
4859
|
+
}
|
|
4860
|
+
async function resolveCredentials(opts) {
|
|
4861
|
+
const context = resolveContext(opts);
|
|
4862
|
+
const cred = await requireAuthCredential(context.org);
|
|
4863
|
+
return { context, cred };
|
|
4864
|
+
}
|
|
4865
|
+
function formatRelationTypes(types) {
|
|
4866
|
+
if (types.length === 0) return "No work item relation types found.";
|
|
4867
|
+
const nameWidth = Math.max(...types.map((t) => t.name.length), 4);
|
|
4868
|
+
const lines = ["Available work item relation types:", ""];
|
|
4869
|
+
for (const t of types) {
|
|
4870
|
+
lines.push(`${t.name.padEnd(nameWidth + 2)}${t.referenceName}`);
|
|
4871
|
+
}
|
|
4872
|
+
return lines.join("\n");
|
|
4873
|
+
}
|
|
4874
|
+
function formatRelationsList(workItemId, relations) {
|
|
4875
|
+
if (relations.length === 0) return `Work item #${workItemId} has no relations.`;
|
|
4876
|
+
const typeWidth = Math.max(...relations.map((r) => r.relName.length), 4) + 2;
|
|
4877
|
+
const idWidth = Math.max(...relations.map((r) => String(r.targetId).length), 4) + 1;
|
|
4878
|
+
const lines = [`Relations for work item #${workItemId}:`, ""];
|
|
4879
|
+
for (const r of relations) {
|
|
4880
|
+
const typeLabel = `[${r.relName}]`.padEnd(typeWidth);
|
|
4881
|
+
const idLabel = `#${r.targetId}`.padEnd(idWidth);
|
|
4882
|
+
lines.push(`${typeLabel} ${idLabel} ${r.targetTitle ?? "(unknown)"}`);
|
|
4883
|
+
}
|
|
4884
|
+
return lines.join("\n");
|
|
4885
|
+
}
|
|
4886
|
+
function handleRelationError(err, id1) {
|
|
4887
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4888
|
+
if (msg === "SELF_RELATION") {
|
|
4889
|
+
process.stderr.write(`Error: cannot relate a work item to itself (#${id1}).
|
|
4890
|
+
`);
|
|
4891
|
+
} else if (msg.startsWith("UNKNOWN_RELATION_TYPE:")) {
|
|
4892
|
+
const name = msg.replace("UNKNOWN_RELATION_TYPE:", "");
|
|
4893
|
+
process.stderr.write(
|
|
4894
|
+
`Error: unknown relation type "${name}". Run 'azdo relations types' to see valid names.
|
|
4895
|
+
`
|
|
4896
|
+
);
|
|
4897
|
+
} else if (msg.startsWith("NOT_FOUND")) {
|
|
4898
|
+
const target = id1 !== void 0 ? id1 : "unknown";
|
|
4899
|
+
process.stderr.write(`Error: work item #${target} not found.
|
|
4900
|
+
`);
|
|
4901
|
+
} else if (msg === "AUTH_FAILED") {
|
|
4902
|
+
process.stderr.write(
|
|
4903
|
+
`Error: authentication failed. Check your PAT has Work Items \u2192 Read & Write scope.
|
|
4904
|
+
`
|
|
4905
|
+
);
|
|
4906
|
+
} else {
|
|
4907
|
+
process.stderr.write(`Error: ${msg}
|
|
4908
|
+
`);
|
|
4909
|
+
}
|
|
4910
|
+
process.exit(1);
|
|
4911
|
+
}
|
|
4912
|
+
function parsePositiveInt(value, label) {
|
|
4913
|
+
const n = Number(value);
|
|
4914
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
4915
|
+
process.stderr.write(`Error: ${label} must be a positive integer.
|
|
4916
|
+
`);
|
|
4917
|
+
process.exit(1);
|
|
4918
|
+
}
|
|
4919
|
+
return n;
|
|
4920
|
+
}
|
|
4921
|
+
function createRelationsCommand() {
|
|
4922
|
+
const relations = new Command16("relations").description("Manage work item relations");
|
|
4923
|
+
addCommonOptions(
|
|
4924
|
+
relations.command("types").description("List all available work item relation types")
|
|
4925
|
+
).action(async (opts) => {
|
|
4926
|
+
try {
|
|
4927
|
+
const { context, cred } = await resolveCredentials(opts);
|
|
4928
|
+
const types = await getWorkItemRelationTypes(context, cred);
|
|
4929
|
+
if (opts.json) {
|
|
4930
|
+
process.stdout.write(JSON.stringify(types, null, 2) + "\n");
|
|
4931
|
+
} else {
|
|
4932
|
+
process.stdout.write(formatRelationTypes(types) + "\n");
|
|
4933
|
+
}
|
|
4934
|
+
} catch (err) {
|
|
4935
|
+
handleRelationError(err);
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
addCommonOptions(
|
|
4939
|
+
relations.command("add <type> <id1> <id2>").description("Add a directed relation from work item <id1> to <id2>")
|
|
4940
|
+
).action(async (type, id1Str, id2Str, opts) => {
|
|
4941
|
+
const id1 = parsePositiveInt(id1Str, "id1");
|
|
4942
|
+
const id2 = parsePositiveInt(id2Str, "id2");
|
|
4943
|
+
try {
|
|
4944
|
+
const { context, cred } = await resolveCredentials(opts);
|
|
4945
|
+
const result = await addWorkItemRelation(context, cred, type, id1, id2);
|
|
4946
|
+
if (opts.json) {
|
|
4947
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4948
|
+
} else if (result.status === "already_exists") {
|
|
4949
|
+
process.stdout.write(`Relation already exists: #${id1} --[${result.type}]--> #${id2}
|
|
4950
|
+
`);
|
|
4951
|
+
} else {
|
|
4952
|
+
process.stdout.write(`Added relation: #${id1} --[${result.type}]--> #${id2}
|
|
4953
|
+
`);
|
|
4954
|
+
}
|
|
4955
|
+
} catch (err) {
|
|
4956
|
+
handleRelationError(err, id1);
|
|
4957
|
+
}
|
|
4958
|
+
});
|
|
4959
|
+
addCommonOptions(
|
|
4960
|
+
relations.command("remove <type> <id1> <id2>").description("Remove a directed relation of <type> from work item <id1> to <id2>")
|
|
4961
|
+
).action(async (type, id1Str, id2Str, opts) => {
|
|
4962
|
+
const id1 = parsePositiveInt(id1Str, "id1");
|
|
4963
|
+
const id2 = parsePositiveInt(id2Str, "id2");
|
|
4964
|
+
try {
|
|
4965
|
+
const { context, cred } = await resolveCredentials(opts);
|
|
4966
|
+
const result = await removeWorkItemRelation(context, cred, type, id1, id2);
|
|
4967
|
+
if (opts.json) {
|
|
4968
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4969
|
+
} else if (result.status === "not_found") {
|
|
4970
|
+
process.stdout.write(`No relation of type '${result.type}' found between #${id1} and #${id2}
|
|
4971
|
+
`);
|
|
4972
|
+
} else {
|
|
4973
|
+
process.stdout.write(`Removed relation: #${id1} --[${result.type}]--> #${id2}
|
|
4974
|
+
`);
|
|
4975
|
+
}
|
|
4976
|
+
} catch (err) {
|
|
4977
|
+
handleRelationError(err, id1);
|
|
4978
|
+
}
|
|
4979
|
+
});
|
|
4980
|
+
addCommonOptions(
|
|
4981
|
+
relations.command("list <id>").description("List all work item link relations on a work item")
|
|
4982
|
+
).action(async (idStr, opts) => {
|
|
4983
|
+
const id = parsePositiveInt(idStr, "id");
|
|
4984
|
+
try {
|
|
4985
|
+
const { context, cred } = await resolveCredentials(opts);
|
|
4986
|
+
const result = await listWorkItemRelations(context, cred, id);
|
|
4987
|
+
if (opts.json) {
|
|
4988
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4989
|
+
} else {
|
|
4990
|
+
process.stdout.write(formatRelationsList(result.workItemId, result.relations) + "\n");
|
|
4991
|
+
}
|
|
4992
|
+
} catch (err) {
|
|
4993
|
+
handleRelationError(err, id);
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4996
|
+
return relations;
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4653
4999
|
// src/services/update-check.ts
|
|
4654
5000
|
import fs2 from "fs";
|
|
4655
5001
|
import path2 from "path";
|
|
@@ -4763,7 +5109,7 @@ function exitOnEpipe(err) {
|
|
|
4763
5109
|
}
|
|
4764
5110
|
process.stdout.on("error", exitOnEpipe);
|
|
4765
5111
|
process.stderr.on("error", exitOnEpipe);
|
|
4766
|
-
var program = new
|
|
5112
|
+
var program = new Command17();
|
|
4767
5113
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
4768
5114
|
program.option("--no-update-check", "Skip the check for a newer published version");
|
|
4769
5115
|
program.addCommand(createGetItemCommand());
|
|
@@ -4781,6 +5127,7 @@ program.addCommand(createPrCommand());
|
|
|
4781
5127
|
program.addCommand(createPipelineCommand());
|
|
4782
5128
|
program.addCommand(createCommentsCommand());
|
|
4783
5129
|
program.addCommand(createDownloadAttachmentCommand());
|
|
5130
|
+
program.addCommand(createRelationsCommand());
|
|
4784
5131
|
program.showHelpAfterError();
|
|
4785
5132
|
program.hook("postAction", async () => {
|
|
4786
5133
|
const notice = await getUpdateNotice({ enabled: program.opts().updateCheck });
|