azdo-cli 0.10.0-develop.394 → 0.10.0-develop.467
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
|
@@ -6,15 +6,19 @@ import {
|
|
|
6
6
|
SETTINGS,
|
|
7
7
|
appendAuthAuditEvent,
|
|
8
8
|
buildScopeString,
|
|
9
|
+
copyOrgScope,
|
|
9
10
|
defaultScopes,
|
|
11
|
+
deleteOrgScope,
|
|
10
12
|
deletePat,
|
|
11
13
|
firstPartyShippedScopes,
|
|
12
14
|
getConfigValue,
|
|
15
|
+
getOrgScopedValue,
|
|
13
16
|
getPat,
|
|
14
17
|
getStoredCredential,
|
|
15
18
|
listOrgsWithStoredPat,
|
|
16
19
|
loadConfig,
|
|
17
20
|
maskedDisplay,
|
|
21
|
+
moveOrgScope,
|
|
18
22
|
normalizePat,
|
|
19
23
|
openUrl,
|
|
20
24
|
probeBackend,
|
|
@@ -22,16 +26,19 @@ import {
|
|
|
22
26
|
readTokenResponse,
|
|
23
27
|
refreshIfNeeded,
|
|
24
28
|
resolveOAuthConfig,
|
|
29
|
+
resolveScopedConfig,
|
|
25
30
|
runAuthCodeFlow,
|
|
26
31
|
setConfigValue,
|
|
32
|
+
setOrgScopedValue,
|
|
27
33
|
storeOAuthCredential,
|
|
28
34
|
storePat,
|
|
29
35
|
tokenResponseToCredential,
|
|
30
|
-
unsetConfigValue
|
|
31
|
-
|
|
36
|
+
unsetConfigValue,
|
|
37
|
+
unsetOrgScopedValue
|
|
38
|
+
} from "./chunk-XVXMDWQE.js";
|
|
32
39
|
|
|
33
40
|
// src/index.ts
|
|
34
|
-
import { Command as
|
|
41
|
+
import { Command as Command16 } from "commander";
|
|
35
42
|
|
|
36
43
|
// src/version.ts
|
|
37
44
|
import { readFileSync } from "fs";
|
|
@@ -245,28 +252,66 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
245
252
|
}
|
|
246
253
|
return await response.json();
|
|
247
254
|
}
|
|
248
|
-
async function
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
|
|
255
|
+
async function getOrgFieldNames(context, cred) {
|
|
256
|
+
const url = new URL(
|
|
257
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/fields`
|
|
258
|
+
);
|
|
259
|
+
url.searchParams.set("api-version", "7.1");
|
|
260
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`HTTP_${response.status}`);
|
|
257
263
|
}
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
return (data.value ?? []).map((f) => f.referenceName);
|
|
266
|
+
}
|
|
267
|
+
function buildCombinedDescription(fields) {
|
|
268
|
+
const parts = [];
|
|
269
|
+
if (fields["System.Description"]) {
|
|
270
|
+
parts.push({ label: "Description", value: fields["System.Description"] });
|
|
260
271
|
}
|
|
261
|
-
if (
|
|
262
|
-
|
|
272
|
+
if (fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
|
|
273
|
+
parts.push({ label: "Acceptance Criteria", value: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
|
|
274
|
+
}
|
|
275
|
+
if (fields["Microsoft.VSTS.TCM.ReproSteps"]) {
|
|
276
|
+
parts.push({ label: "Repro Steps", value: fields["Microsoft.VSTS.TCM.ReproSteps"] });
|
|
277
|
+
}
|
|
278
|
+
if (parts.length === 0) return null;
|
|
279
|
+
if (parts.length === 1) return parts[0].value;
|
|
280
|
+
return parts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
|
|
281
|
+
}
|
|
282
|
+
async function fetchWorkItemWithFallback(context, id, cred, normalizedExtraFields) {
|
|
283
|
+
try {
|
|
284
|
+
const data = await fetchWorkItemResponse(context, id, cred, {
|
|
285
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
|
|
286
|
+
});
|
|
287
|
+
return { data, effectiveExtraFields: normalizedExtraFields };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
290
|
+
if (!msg.includes("TF51535")) throw err;
|
|
291
|
+
const orgFieldNames = await getOrgFieldNames(context, cred);
|
|
292
|
+
const orgFieldsLower = new Set(orgFieldNames.map((n) => n.toLowerCase()));
|
|
293
|
+
const missing = normalizedExtraFields.filter((f) => !orgFieldsLower.has(f.toLowerCase()));
|
|
294
|
+
const effectiveExtraFields = normalizedExtraFields.filter((f) => orgFieldsLower.has(f.toLowerCase()));
|
|
295
|
+
for (const f of missing) {
|
|
296
|
+
process.stderr.write(`azdo: warning: field '${f}' does not exist in organization '${context.org}' and was skipped
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
const data = await fetchWorkItemResponse(context, id, cred, {
|
|
300
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...effectiveExtraFields])
|
|
301
|
+
});
|
|
302
|
+
return { data, effectiveExtraFields };
|
|
263
303
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
304
|
+
}
|
|
305
|
+
async function getWorkItem(context, id, cred, extraFields) {
|
|
306
|
+
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
307
|
+
let effectiveExtraFields = normalizedExtraFields;
|
|
308
|
+
let data;
|
|
309
|
+
if (normalizedExtraFields.length > 0) {
|
|
310
|
+
({ data, effectiveExtraFields } = await fetchWorkItemWithFallback(context, id, cred, normalizedExtraFields));
|
|
311
|
+
} else {
|
|
312
|
+
data = await fetchWorkItemResponse(context, id, cred, { includeRelations: true });
|
|
269
313
|
}
|
|
314
|
+
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, { includeRelations: true }) : data;
|
|
270
315
|
return {
|
|
271
316
|
id: data.id,
|
|
272
317
|
rev: data.rev,
|
|
@@ -274,11 +319,11 @@ async function getWorkItem(context, id, cred, extraFields) {
|
|
|
274
319
|
state: data.fields["System.State"],
|
|
275
320
|
type: data.fields["System.WorkItemType"],
|
|
276
321
|
assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
|
|
277
|
-
description:
|
|
322
|
+
description: buildCombinedDescription(data.fields),
|
|
278
323
|
areaPath: data.fields["System.AreaPath"],
|
|
279
324
|
iterationPath: data.fields["System.IterationPath"],
|
|
280
325
|
url: data._links.html.href,
|
|
281
|
-
extraFields:
|
|
326
|
+
extraFields: effectiveExtraFields.length > 0 ? buildExtraFields(data.fields, effectiveExtraFields) : null,
|
|
282
327
|
attachments: extractAttachments(relationsData.relations)
|
|
283
328
|
};
|
|
284
329
|
}
|
|
@@ -479,13 +524,13 @@ async function classifyDeviceTokenResponse(response) {
|
|
|
479
524
|
async function pollForDeviceToken(deviceCode, oauthConfig, initialIntervalSec, expiresAtMs, deps) {
|
|
480
525
|
const fetchFn = deps.fetch ?? fetch;
|
|
481
526
|
const now = deps.now ?? (() => Date.now());
|
|
482
|
-
const
|
|
527
|
+
const sleep2 = deps.sleep ?? defaultSleep;
|
|
483
528
|
let intervalSec = Math.max(MIN_INTERVAL_SEC, initialIntervalSec);
|
|
484
529
|
for (; ; ) {
|
|
485
530
|
if (now() >= expiresAtMs) {
|
|
486
531
|
throw new DeviceCodeFlowError("expired_token", "device-code flow expired before authorisation completed");
|
|
487
532
|
}
|
|
488
|
-
await
|
|
533
|
+
await sleep2(intervalSec * 1e3);
|
|
489
534
|
const body = new URLSearchParams({
|
|
490
535
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
491
536
|
client_id: oauthConfig.clientId,
|
|
@@ -765,23 +810,39 @@ async function status() {
|
|
|
765
810
|
}
|
|
766
811
|
|
|
767
812
|
// src/services/git-remote.ts
|
|
768
|
-
import {
|
|
813
|
+
import { execFileSync } from "child_process";
|
|
814
|
+
import fs from "fs";
|
|
815
|
+
import path from "path";
|
|
769
816
|
|
|
770
817
|
// src/services/remote-warning.ts
|
|
771
|
-
var WARNING = "azdo: warning: origin includes embedded credentials; consider removing them with 'git remote set-url origin <clean-url>'\n";
|
|
772
818
|
var warned = false;
|
|
773
|
-
function
|
|
819
|
+
function buildWarning(remoteName) {
|
|
820
|
+
return `azdo: warning: ${remoteName} includes embedded credentials; consider removing them with 'git remote set-url ${remoteName} <clean-url>'
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
function noticeCredentialBearingRemote(remoteName = "origin") {
|
|
774
824
|
if (warned) {
|
|
775
825
|
return;
|
|
776
826
|
}
|
|
777
827
|
warned = true;
|
|
778
828
|
try {
|
|
779
|
-
process.stderr.write(
|
|
829
|
+
process.stderr.write(buildWarning(remoteName));
|
|
780
830
|
} catch {
|
|
781
831
|
}
|
|
782
832
|
}
|
|
783
833
|
|
|
784
834
|
// src/services/git-remote.ts
|
|
835
|
+
var GIT_BINARY = (() => {
|
|
836
|
+
const known = process.platform === "win32" ? [String.raw`C:\Program Files\Git\bin\git.exe`, String.raw`C:\Program Files (x86)\Git\bin\git.exe`] : ["/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git"];
|
|
837
|
+
return known.find((p) => {
|
|
838
|
+
try {
|
|
839
|
+
execFileSync(p, ["--version"], { stdio: "ignore" });
|
|
840
|
+
return true;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}) ?? "git";
|
|
845
|
+
})();
|
|
785
846
|
var patterns = [
|
|
786
847
|
// HTTPS (current): https://[user[:token]@]dev.azure.com/{org}/{project}/_git/{repo}[.git]
|
|
787
848
|
/^https?:\/\/(?:[^@/]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/,
|
|
@@ -794,41 +855,137 @@ var patterns = [
|
|
|
794
855
|
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}[.git]
|
|
795
856
|
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/
|
|
796
857
|
];
|
|
797
|
-
var
|
|
798
|
-
function
|
|
858
|
+
var httpsEmbeddedSecret = /^https?:\/\/[^:@/]+:[^@/]+@/;
|
|
859
|
+
function parseSingleRemoteLine(line) {
|
|
860
|
+
const trimmed = line.trim();
|
|
861
|
+
if (!trimmed) return null;
|
|
862
|
+
const tabIdx = trimmed.indexOf(" ");
|
|
863
|
+
if (tabIdx === -1) return null;
|
|
864
|
+
const remoteName = trimmed.slice(0, tabIdx);
|
|
865
|
+
const afterTab = trimmed.slice(tabIdx + 1);
|
|
866
|
+
const urlEnd = afterTab.lastIndexOf(" (");
|
|
867
|
+
const url = urlEnd === -1 ? afterTab : afterTab.slice(0, urlEnd);
|
|
868
|
+
return { remoteName, url };
|
|
869
|
+
}
|
|
870
|
+
function matchAzdoRemote(remoteName, url) {
|
|
799
871
|
for (const pattern of patterns) {
|
|
800
872
|
const match = pattern.exec(url);
|
|
801
|
-
if (match)
|
|
802
|
-
|
|
803
|
-
|
|
873
|
+
if (!match) continue;
|
|
874
|
+
const project = match[2];
|
|
875
|
+
if (/^DefaultCollection$/i.test(project)) return null;
|
|
876
|
+
return { remoteName, org: match[1], project, hasEmbeddedSecret: httpsEmbeddedSecret.test(url) };
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
function parseAllAzdoRemotes(output) {
|
|
881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
882
|
+
const results = [];
|
|
883
|
+
for (const line of output.split("\n")) {
|
|
884
|
+
const parsed = parseSingleRemoteLine(line);
|
|
885
|
+
if (!parsed) continue;
|
|
886
|
+
const { remoteName, url } = parsed;
|
|
887
|
+
if (seen.has(remoteName)) continue;
|
|
888
|
+
const candidate = matchAzdoRemote(remoteName, url);
|
|
889
|
+
if (candidate) {
|
|
890
|
+
seen.add(remoteName);
|
|
891
|
+
results.push(candidate);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return results;
|
|
895
|
+
}
|
|
896
|
+
function selectRemote(candidates) {
|
|
897
|
+
if (candidates.length === 0) {
|
|
898
|
+
throw new Error("No Azure DevOps remote found. Provide --org and --project explicitly.");
|
|
899
|
+
}
|
|
900
|
+
const origin = candidates.find((c) => c.remoteName === "origin");
|
|
901
|
+
if (origin) return origin;
|
|
902
|
+
if (candidates.length === 1) return candidates[0];
|
|
903
|
+
const first = candidates[0];
|
|
904
|
+
const allSame = candidates.every(
|
|
905
|
+
(c) => c.org.toLowerCase() === first.org.toLowerCase() && c.project.toLowerCase() === first.project.toLowerCase()
|
|
906
|
+
);
|
|
907
|
+
if (allSame) return first;
|
|
908
|
+
const lines = candidates.map((c) => ` ${c.remoteName.padEnd(8)} \u2192 ${c.org}/${c.project}`).join("\n");
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Multiple Azure DevOps remotes found with different org/project:
|
|
911
|
+
${lines}
|
|
912
|
+
Use --org/--project (or 'git remote rename <name> origin') to disambiguate.`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function readGitConfigContent() {
|
|
916
|
+
const gitDirEnv = process.env.GIT_DIR;
|
|
917
|
+
if (gitDirEnv) {
|
|
918
|
+
return fs.readFileSync(path.join(gitDirEnv, "config"), "utf-8");
|
|
919
|
+
}
|
|
920
|
+
let dir = process.cwd();
|
|
921
|
+
for (; ; ) {
|
|
922
|
+
const gitPath = path.join(dir, ".git");
|
|
923
|
+
try {
|
|
924
|
+
const stat = fs.statSync(gitPath);
|
|
925
|
+
if (stat.isDirectory()) {
|
|
926
|
+
return fs.readFileSync(path.join(gitPath, "config"), "utf-8");
|
|
804
927
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
928
|
+
if (stat.isFile()) {
|
|
929
|
+
const ref = fs.readFileSync(gitPath, "utf-8");
|
|
930
|
+
const m = /^gitdir:[ \t]*([^\r\n]+)/m.exec(ref);
|
|
931
|
+
if (m) {
|
|
932
|
+
return fs.readFileSync(path.join(path.resolve(dir, m[1].trim()), "config"), "utf-8");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
const parent = path.dirname(dir);
|
|
938
|
+
if (parent === dir) break;
|
|
939
|
+
dir = parent;
|
|
940
|
+
}
|
|
941
|
+
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
942
|
+
}
|
|
943
|
+
function gitConfigToRemoteLines(configContent) {
|
|
944
|
+
const lines = [];
|
|
945
|
+
let currentRemote = null;
|
|
946
|
+
let emittedUrl = false;
|
|
947
|
+
for (const line of configContent.split("\n")) {
|
|
948
|
+
const sectionMatch = /^\[remote\s+"([^"]+)"\]/.exec(line);
|
|
949
|
+
if (sectionMatch) {
|
|
950
|
+
currentRemote = sectionMatch[1];
|
|
951
|
+
emittedUrl = false;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (line.startsWith("[")) {
|
|
955
|
+
currentRemote = null;
|
|
956
|
+
emittedUrl = false;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (currentRemote && !emittedUrl) {
|
|
960
|
+
const urlMatch = /^[ \t]+url[ \t]*=[ \t]*([^\r\n]+)/.exec(line);
|
|
961
|
+
if (urlMatch) {
|
|
962
|
+
lines.push(`${currentRemote} ${urlMatch[1].trim()} (fetch)`);
|
|
963
|
+
emittedUrl = true;
|
|
808
964
|
}
|
|
809
|
-
return { org: match[1], project };
|
|
810
965
|
}
|
|
811
966
|
}
|
|
812
|
-
return
|
|
967
|
+
return lines.join("\n");
|
|
813
968
|
}
|
|
814
969
|
function detectAzdoContext() {
|
|
815
|
-
let
|
|
970
|
+
let configContent;
|
|
816
971
|
try {
|
|
817
|
-
|
|
972
|
+
configContent = readGitConfigContent();
|
|
818
973
|
} catch {
|
|
819
974
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
820
975
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
976
|
+
const remoteLines = gitConfigToRemoteLines(configContent);
|
|
977
|
+
const candidates = parseAllAzdoRemotes(remoteLines);
|
|
978
|
+
const selected = selectRemote(candidates);
|
|
979
|
+
if (selected.hasEmbeddedSecret) {
|
|
980
|
+
noticeCredentialBearingRemote(selected.remoteName);
|
|
824
981
|
}
|
|
825
|
-
return
|
|
982
|
+
return { org: selected.org, project: selected.project };
|
|
826
983
|
}
|
|
827
984
|
function parseRepoName(url) {
|
|
828
985
|
for (const pattern of patterns) {
|
|
829
986
|
const match = pattern.exec(url);
|
|
830
987
|
if (match) {
|
|
831
|
-
if (
|
|
988
|
+
if (httpsEmbeddedSecret.test(url)) {
|
|
832
989
|
noticeCredentialBearingRemote();
|
|
833
990
|
}
|
|
834
991
|
return match[3];
|
|
@@ -839,7 +996,7 @@ function parseRepoName(url) {
|
|
|
839
996
|
function detectRepoName() {
|
|
840
997
|
let remoteUrl;
|
|
841
998
|
try {
|
|
842
|
-
remoteUrl =
|
|
999
|
+
remoteUrl = execFileSync(GIT_BINARY, ["remote", "get-url", "origin"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
843
1000
|
} catch {
|
|
844
1001
|
throw new Error('Not in a git repository. Check that git remote "origin" exists and try again.');
|
|
845
1002
|
}
|
|
@@ -850,7 +1007,7 @@ function detectRepoName() {
|
|
|
850
1007
|
return repo;
|
|
851
1008
|
}
|
|
852
1009
|
function getCurrentBranch() {
|
|
853
|
-
const branch =
|
|
1010
|
+
const branch = execFileSync(GIT_BINARY, ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
854
1011
|
if (branch === "HEAD") {
|
|
855
1012
|
throw new Error("Not on a named branch. Check out a named branch and try again.");
|
|
856
1013
|
}
|
|
@@ -886,7 +1043,7 @@ function formatResolutionError() {
|
|
|
886
1043
|
return [
|
|
887
1044
|
"Could not resolve an Azure DevOps organization. Options (in priority order):",
|
|
888
1045
|
" 1. Pass --org <name> on the command line.",
|
|
889
|
-
" 2. Run this command from a git repo
|
|
1046
|
+
" 2. Run this command from a git repo that has an Azure DevOps remote.",
|
|
890
1047
|
" 3. Run `azdo config set org <name>` once to set a persistent default."
|
|
891
1048
|
].join("\n");
|
|
892
1049
|
}
|
|
@@ -899,9 +1056,9 @@ function resolveContext(options) {
|
|
|
899
1056
|
gitContext = detectAzdoContext();
|
|
900
1057
|
} catch {
|
|
901
1058
|
}
|
|
902
|
-
const config = loadConfig();
|
|
903
1059
|
const org = resolvedOrg?.org;
|
|
904
|
-
const
|
|
1060
|
+
const scopedCfg = resolveScopedConfig(org);
|
|
1061
|
+
const project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || scopedCfg.project;
|
|
905
1062
|
if (org && project) {
|
|
906
1063
|
return { org, project };
|
|
907
1064
|
}
|
|
@@ -1354,9 +1511,10 @@ function createGetItemCommand() {
|
|
|
1354
1511
|
try {
|
|
1355
1512
|
context = resolveContext(options);
|
|
1356
1513
|
const credential = await requireAuthCredential(context.org);
|
|
1357
|
-
const
|
|
1514
|
+
const scopedCfg = resolveScopedConfig(context.org);
|
|
1515
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
|
|
1358
1516
|
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1359
|
-
const markdownEnabled = options.markdown ??
|
|
1517
|
+
const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
|
|
1360
1518
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1361
1519
|
process.stdout.write(output + "\n");
|
|
1362
1520
|
if (imageOptions.enabled) {
|
|
@@ -1836,18 +1994,47 @@ function formatConfigValue(value, unsetFallback = "") {
|
|
|
1836
1994
|
}
|
|
1837
1995
|
return Array.isArray(value) ? value.join(",") : value;
|
|
1838
1996
|
}
|
|
1997
|
+
function buildConfigListEntries(cfg) {
|
|
1998
|
+
const entries = SETTINGS.map((s) => ({
|
|
1999
|
+
scope: "default",
|
|
2000
|
+
key: s.key,
|
|
2001
|
+
value: cfg[s.key]
|
|
2002
|
+
}));
|
|
2003
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
2004
|
+
for (const [k, v] of Object.entries(scope)) {
|
|
2005
|
+
entries.push({ scope: orgName, key: k, value: v });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return entries;
|
|
2009
|
+
}
|
|
1839
2010
|
function writeConfigList(cfg) {
|
|
1840
2011
|
const keyWidth = 10;
|
|
1841
2012
|
const valueWidth = 30;
|
|
2013
|
+
const scopeWidth = 12;
|
|
2014
|
+
process.stdout.write(
|
|
2015
|
+
`${"scope".padEnd(scopeWidth)}${"key".padEnd(keyWidth)}${"value".padEnd(valueWidth)}description
|
|
2016
|
+
`
|
|
2017
|
+
);
|
|
1842
2018
|
for (const setting of SETTINGS) {
|
|
1843
2019
|
const raw = cfg[setting.key];
|
|
1844
2020
|
const value = formatConfigValue(raw, "(not set)");
|
|
1845
2021
|
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
1846
2022
|
process.stdout.write(
|
|
1847
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
2023
|
+
`${"default".padEnd(scopeWidth)}${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1848
2024
|
`
|
|
1849
2025
|
);
|
|
1850
2026
|
}
|
|
2027
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
2028
|
+
const orgScope = scope;
|
|
2029
|
+
const scopedSettings = Object.entries(orgScope);
|
|
2030
|
+
for (const [k, v] of scopedSettings) {
|
|
2031
|
+
const value = formatConfigValue(v, "(not set)");
|
|
2032
|
+
process.stdout.write(
|
|
2033
|
+
`${orgName.padEnd(scopeWidth)}${k.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}
|
|
2034
|
+
`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1851
2038
|
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
1852
2039
|
if (hasUnset) {
|
|
1853
2040
|
process.stdout.write(
|
|
@@ -1891,17 +2078,22 @@ function createConfigCommand() {
|
|
|
1891
2078
|
const config = new Command4("config");
|
|
1892
2079
|
config.description("Manage CLI settings");
|
|
1893
2080
|
const set = new Command4("set");
|
|
1894
|
-
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
|
|
2081
|
+
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
|
|
1895
2082
|
try {
|
|
1896
|
-
|
|
2083
|
+
if (options.org) {
|
|
2084
|
+
setOrgScopedValue(options.org, key, value);
|
|
2085
|
+
} else {
|
|
2086
|
+
setConfigValue(key, value);
|
|
2087
|
+
}
|
|
1897
2088
|
if (options.json) {
|
|
1898
|
-
const output = { key, value };
|
|
2089
|
+
const output = { key, value, scope: options.org ?? "default" };
|
|
1899
2090
|
if (key === "fields") {
|
|
1900
2091
|
output.value = value.split(",").map((s) => s.trim());
|
|
1901
2092
|
}
|
|
1902
2093
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
1903
2094
|
} else {
|
|
1904
|
-
|
|
2095
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2096
|
+
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
1905
2097
|
`);
|
|
1906
2098
|
}
|
|
1907
2099
|
} catch (err) {
|
|
@@ -1912,12 +2104,12 @@ function createConfigCommand() {
|
|
|
1912
2104
|
}
|
|
1913
2105
|
});
|
|
1914
2106
|
const get = new Command4("get");
|
|
1915
|
-
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2107
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1916
2108
|
try {
|
|
1917
|
-
const value = getConfigValue(key);
|
|
2109
|
+
const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
|
|
1918
2110
|
if (options.json) {
|
|
1919
2111
|
process.stdout.write(
|
|
1920
|
-
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
2112
|
+
JSON.stringify({ key, value: value ?? null, scope: options.org ?? "default" }) + "\n"
|
|
1921
2113
|
);
|
|
1922
2114
|
} else if (value === void 0) {
|
|
1923
2115
|
process.stdout.write(`Setting "${key}" is not configured.
|
|
@@ -1925,7 +2117,7 @@ function createConfigCommand() {
|
|
|
1925
2117
|
} else if (Array.isArray(value)) {
|
|
1926
2118
|
process.stdout.write(value.join(",") + "\n");
|
|
1927
2119
|
} else {
|
|
1928
|
-
process.stdout.write(value + "\n");
|
|
2120
|
+
process.stdout.write(String(value) + "\n");
|
|
1929
2121
|
}
|
|
1930
2122
|
} catch (err) {
|
|
1931
2123
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1938,19 +2130,25 @@ function createConfigCommand() {
|
|
|
1938
2130
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1939
2131
|
const cfg = loadConfig();
|
|
1940
2132
|
if (options.json) {
|
|
1941
|
-
|
|
2133
|
+
const entries = buildConfigListEntries(cfg);
|
|
2134
|
+
process.stdout.write(JSON.stringify(entries) + "\n");
|
|
1942
2135
|
return;
|
|
1943
2136
|
}
|
|
1944
2137
|
writeConfigList(cfg);
|
|
1945
2138
|
});
|
|
1946
2139
|
const unset = new Command4("unset");
|
|
1947
|
-
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2140
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1948
2141
|
try {
|
|
1949
|
-
|
|
2142
|
+
if (options.org) {
|
|
2143
|
+
unsetOrgScopedValue(options.org, key);
|
|
2144
|
+
} else {
|
|
2145
|
+
unsetConfigValue(key);
|
|
2146
|
+
}
|
|
1950
2147
|
if (options.json) {
|
|
1951
|
-
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
2148
|
+
process.stdout.write(JSON.stringify({ key, unset: true, scope: options.org ?? "default" }) + "\n");
|
|
1952
2149
|
} else {
|
|
1953
|
-
|
|
2150
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2151
|
+
process.stdout.write(`Unset "${key}"${scopeTag}
|
|
1954
2152
|
`);
|
|
1955
2153
|
}
|
|
1956
2154
|
} catch (err) {
|
|
@@ -1982,10 +2180,52 @@ function createConfigCommand() {
|
|
|
1982
2180
|
rl.close();
|
|
1983
2181
|
process.stderr.write("Configuration complete!\n");
|
|
1984
2182
|
});
|
|
2183
|
+
const orgCopy = new Command4("org-copy");
|
|
2184
|
+
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) => {
|
|
2185
|
+
try {
|
|
2186
|
+
copyOrgScope(from, to, options.force ?? false);
|
|
2187
|
+
process.stdout.write(`Copied scope "${from}" to "${to}"
|
|
2188
|
+
`);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2191
|
+
process.stderr.write(`Error: ${message}
|
|
2192
|
+
`);
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
const orgMove = new Command4("org-move");
|
|
2197
|
+
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) => {
|
|
2198
|
+
try {
|
|
2199
|
+
moveOrgScope(from, to, options.force ?? false);
|
|
2200
|
+
process.stdout.write(`Moved scope "${from}" to "${to}"
|
|
2201
|
+
`);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2204
|
+
process.stderr.write(`Error: ${message}
|
|
2205
|
+
`);
|
|
2206
|
+
process.exit(1);
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
const orgDelete = new Command4("org-delete");
|
|
2210
|
+
orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
|
|
2211
|
+
try {
|
|
2212
|
+
deleteOrgScope(name);
|
|
2213
|
+
process.stdout.write(`Deleted scope "${name}"
|
|
2214
|
+
`);
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2217
|
+
process.stderr.write(`Error: ${message}
|
|
2218
|
+
`);
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
1985
2222
|
config.addCommand(set);
|
|
1986
2223
|
config.addCommand(get);
|
|
1987
2224
|
config.addCommand(list);
|
|
1988
2225
|
config.addCommand(unset);
|
|
2226
|
+
config.addCommand(orgCopy);
|
|
2227
|
+
config.addCommand(orgMove);
|
|
2228
|
+
config.addCommand(orgDelete);
|
|
1989
2229
|
config.addCommand(wizard);
|
|
1990
2230
|
return config;
|
|
1991
2231
|
}
|
|
@@ -3368,10 +3608,922 @@ function createPrCommand() {
|
|
|
3368
3608
|
return command;
|
|
3369
3609
|
}
|
|
3370
3610
|
|
|
3371
|
-
// src/commands/
|
|
3611
|
+
// src/commands/pipeline.ts
|
|
3372
3612
|
import { Command as Command13 } from "commander";
|
|
3613
|
+
|
|
3614
|
+
// src/services/pipeline-client.ts
|
|
3615
|
+
var API_VERSION = "7.1";
|
|
3616
|
+
function orgProjectBase(context) {
|
|
3617
|
+
return `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}`;
|
|
3618
|
+
}
|
|
3619
|
+
function withApiVersion(url) {
|
|
3620
|
+
url.searchParams.set("api-version", API_VERSION);
|
|
3621
|
+
return url;
|
|
3622
|
+
}
|
|
3623
|
+
async function readJsonResponse2(response) {
|
|
3624
|
+
if (!response.ok) {
|
|
3625
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3626
|
+
}
|
|
3627
|
+
return await response.json();
|
|
3628
|
+
}
|
|
3629
|
+
function mapPipeline(pipeline) {
|
|
3630
|
+
return {
|
|
3631
|
+
id: pipeline.id,
|
|
3632
|
+
name: pipeline.name,
|
|
3633
|
+
folder: pipeline.folder?.trim() ? pipeline.folder : null
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
function mapRunState(state) {
|
|
3637
|
+
if (state === "inProgress" || state === "completed") {
|
|
3638
|
+
return state;
|
|
3639
|
+
}
|
|
3640
|
+
return "unknown";
|
|
3641
|
+
}
|
|
3642
|
+
function mapRunResult(result) {
|
|
3643
|
+
if (result === "succeeded" || result === "failed" || result === "canceled") {
|
|
3644
|
+
return result;
|
|
3645
|
+
}
|
|
3646
|
+
if (result === "partiallySucceeded") {
|
|
3647
|
+
return "failed";
|
|
3648
|
+
}
|
|
3649
|
+
return null;
|
|
3650
|
+
}
|
|
3651
|
+
function mapBuildState(status2) {
|
|
3652
|
+
if (status2 === "completed") return "completed";
|
|
3653
|
+
if (status2 === void 0 || status2 === "none") return "unknown";
|
|
3654
|
+
return "inProgress";
|
|
3655
|
+
}
|
|
3656
|
+
function mapBuildSummary(build) {
|
|
3657
|
+
return {
|
|
3658
|
+
id: build.id,
|
|
3659
|
+
name: build.buildNumber ?? null,
|
|
3660
|
+
state: mapBuildState(build.status),
|
|
3661
|
+
result: mapRunResult(build.result),
|
|
3662
|
+
createdDate: build.queueTime ?? build.startTime ?? null,
|
|
3663
|
+
finishedDate: build.finishTime ?? null,
|
|
3664
|
+
sourceBranch: build.sourceBranch ?? null,
|
|
3665
|
+
sourceCommit: build.sourceVersion ?? null
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
function normalizeRef(branch) {
|
|
3669
|
+
return branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
|
|
3670
|
+
}
|
|
3671
|
+
async function getPipelineDefinitions(context, cred) {
|
|
3672
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/pipelines`));
|
|
3673
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3674
|
+
const data = await readJsonResponse2(response);
|
|
3675
|
+
return data.value.map(mapPipeline);
|
|
3676
|
+
}
|
|
3677
|
+
var COMMIT_LOOKBACK = 200;
|
|
3678
|
+
async function getPipelineRuns(context, cred, query) {
|
|
3679
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds`));
|
|
3680
|
+
if (query.definitionId !== void 0) {
|
|
3681
|
+
url.searchParams.set("definitions", String(query.definitionId));
|
|
3682
|
+
}
|
|
3683
|
+
if (query.prNumber !== void 0) {
|
|
3684
|
+
url.searchParams.set("branchName", `refs/pull/${query.prNumber}/merge`);
|
|
3685
|
+
} else if (query.branch) {
|
|
3686
|
+
url.searchParams.set("branchName", normalizeRef(query.branch));
|
|
3687
|
+
}
|
|
3688
|
+
url.searchParams.set("queryOrder", "queueTimeDescending");
|
|
3689
|
+
url.searchParams.set("$top", String(query.commit ? COMMIT_LOOKBACK : query.top));
|
|
3690
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3691
|
+
const data = await readJsonResponse2(response);
|
|
3692
|
+
let runs = data.value.map(mapBuildSummary);
|
|
3693
|
+
if (query.commit) {
|
|
3694
|
+
const needle = query.commit.toLowerCase();
|
|
3695
|
+
runs = runs.filter((run) => run.sourceCommit?.toLowerCase().startsWith(needle));
|
|
3696
|
+
}
|
|
3697
|
+
return runs.slice(0, query.top);
|
|
3698
|
+
}
|
|
3699
|
+
async function runPipeline(context, cred, pipelineId, opts) {
|
|
3700
|
+
const url = withApiVersion(
|
|
3701
|
+
new URL(`${orgProjectBase(context)}/_apis/pipelines/${pipelineId}/runs`)
|
|
3702
|
+
);
|
|
3703
|
+
const body = {};
|
|
3704
|
+
if (opts.branch) {
|
|
3705
|
+
const refName = opts.branch.startsWith("refs/") ? opts.branch : `refs/heads/${opts.branch}`;
|
|
3706
|
+
body.resources = { repositories: { self: { refName } } };
|
|
3707
|
+
}
|
|
3708
|
+
if (opts.parameters && Object.keys(opts.parameters).length > 0) {
|
|
3709
|
+
body.templateParameters = opts.parameters;
|
|
3710
|
+
}
|
|
3711
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3712
|
+
method: "POST",
|
|
3713
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
3714
|
+
body: JSON.stringify(body)
|
|
3715
|
+
});
|
|
3716
|
+
const data = await readJsonResponse2(response);
|
|
3717
|
+
return {
|
|
3718
|
+
id: data.id,
|
|
3719
|
+
state: mapRunState(data.state),
|
|
3720
|
+
webUrl: data._links?.web?.href ?? null
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
function buildUrl(context, buildId) {
|
|
3724
|
+
return withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}`));
|
|
3725
|
+
}
|
|
3726
|
+
async function getBuild(context, cred, buildId) {
|
|
3727
|
+
const response = await fetchWithErrors(buildUrl(context, buildId).toString(), {
|
|
3728
|
+
headers: authHeaders(cred)
|
|
3729
|
+
});
|
|
3730
|
+
return readJsonResponse2(response);
|
|
3731
|
+
}
|
|
3732
|
+
async function getBuildStatus(context, cred, buildId) {
|
|
3733
|
+
const build = await getBuild(context, cred, buildId);
|
|
3734
|
+
return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
|
|
3735
|
+
}
|
|
3736
|
+
async function getBuildTimeline(context, cred, buildId) {
|
|
3737
|
+
const url = withApiVersion(
|
|
3738
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
|
|
3739
|
+
);
|
|
3740
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3741
|
+
const data = await readJsonResponse2(response);
|
|
3742
|
+
const records = data.records ?? [];
|
|
3743
|
+
const errors = [];
|
|
3744
|
+
const stages = [];
|
|
3745
|
+
const jobs = [];
|
|
3746
|
+
const logSteps = /* @__PURE__ */ new Map();
|
|
3747
|
+
for (const record of records) {
|
|
3748
|
+
for (const issue of record.issues ?? []) {
|
|
3749
|
+
if (issue.type === "error" && issue.message) {
|
|
3750
|
+
errors.push({ message: issue.message, source: record.name ?? null });
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
if (record.name && record.log?.id !== void 0) {
|
|
3754
|
+
logSteps.set(record.log.id, record.name);
|
|
3755
|
+
}
|
|
3756
|
+
if (!record.name) continue;
|
|
3757
|
+
const status2 = {
|
|
3758
|
+
name: record.name,
|
|
3759
|
+
state: record.state ?? "unknown",
|
|
3760
|
+
result: record.result ?? null
|
|
3761
|
+
};
|
|
3762
|
+
if (record.type === "Stage") {
|
|
3763
|
+
stages.push(status2);
|
|
3764
|
+
} else if (record.type === "Job") {
|
|
3765
|
+
jobs.push({ startTime: record.startTime, status: status2 });
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
jobs.sort((a, b) => {
|
|
3769
|
+
if (a.startTime === b.startTime) return 0;
|
|
3770
|
+
if (a.startTime === void 0) return 1;
|
|
3771
|
+
if (b.startTime === void 0) return -1;
|
|
3772
|
+
return a.startTime < b.startTime ? -1 : 1;
|
|
3773
|
+
});
|
|
3774
|
+
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
|
|
3775
|
+
}
|
|
3776
|
+
async function listTestRuns(context, cred, buildId) {
|
|
3777
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
|
|
3778
|
+
url.searchParams.set("buildUri", `vstfs:///Build/Build/${buildId}`);
|
|
3779
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3780
|
+
const data = await readJsonResponse2(response);
|
|
3781
|
+
return data.value;
|
|
3782
|
+
}
|
|
3783
|
+
async function getTestSummary(context, cred, buildId) {
|
|
3784
|
+
const testRuns = await listTestRuns(context, cred, buildId);
|
|
3785
|
+
let total = 0;
|
|
3786
|
+
let failed = 0;
|
|
3787
|
+
for (const run of testRuns) {
|
|
3788
|
+
const runTotal = run.totalTests ?? 0;
|
|
3789
|
+
total += runTotal;
|
|
3790
|
+
const passedOrSkipped = (run.passedTests ?? 0) + (run.notApplicableTests ?? 0) + (run.incompleteTests ?? 0);
|
|
3791
|
+
failed += Math.max(0, runTotal - passedOrSkipped);
|
|
3792
|
+
}
|
|
3793
|
+
if (total === 0) {
|
|
3794
|
+
return { present: false, total: 0, failed: 0, failedTests: [] };
|
|
3795
|
+
}
|
|
3796
|
+
return { present: true, total, failed, failedTests: [] };
|
|
3797
|
+
}
|
|
3798
|
+
var MAX_FAILED_TESTS = 50;
|
|
3799
|
+
async function getFailedTests(context, cred, buildId) {
|
|
3800
|
+
const testRuns = await listTestRuns(context, cred, buildId);
|
|
3801
|
+
const failed = [];
|
|
3802
|
+
for (const testRun of testRuns) {
|
|
3803
|
+
if (failed.length >= MAX_FAILED_TESTS) break;
|
|
3804
|
+
const resultsUrl = withApiVersion(
|
|
3805
|
+
new URL(`${orgProjectBase(context)}/_apis/test/runs/${testRun.id}/results`)
|
|
3806
|
+
);
|
|
3807
|
+
resultsUrl.searchParams.set("outcomes", "Failed");
|
|
3808
|
+
resultsUrl.searchParams.set("$top", String(MAX_FAILED_TESTS - failed.length));
|
|
3809
|
+
const resultsResponse = await fetchWithErrors(resultsUrl.toString(), {
|
|
3810
|
+
headers: authHeaders(cred)
|
|
3811
|
+
});
|
|
3812
|
+
const resultsData = await readJsonResponse2(resultsResponse);
|
|
3813
|
+
for (const result of resultsData.value) {
|
|
3814
|
+
failed.push({
|
|
3815
|
+
name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
|
|
3816
|
+
errorMessage: result.errorMessage ?? null
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
return failed;
|
|
3821
|
+
}
|
|
3822
|
+
function secondsBetween(start, finish) {
|
|
3823
|
+
if (!start || !finish) return null;
|
|
3824
|
+
const ms = Date.parse(finish) - Date.parse(start);
|
|
3825
|
+
return Number.isFinite(ms) ? Math.round(ms / 1e3) : null;
|
|
3826
|
+
}
|
|
3827
|
+
async function getRunDetail(context, cred, buildId) {
|
|
3828
|
+
const build = await getBuild(context, cred, buildId);
|
|
3829
|
+
let errors = [];
|
|
3830
|
+
let stages = [];
|
|
3831
|
+
let jobs = [];
|
|
3832
|
+
let errorsAvailable = true;
|
|
3833
|
+
try {
|
|
3834
|
+
const timeline = await getBuildTimeline(context, cred, buildId);
|
|
3835
|
+
errors = timeline.errors;
|
|
3836
|
+
stages = timeline.stages;
|
|
3837
|
+
jobs = timeline.jobs;
|
|
3838
|
+
} catch {
|
|
3839
|
+
errorsAvailable = false;
|
|
3840
|
+
}
|
|
3841
|
+
let tests = { present: false, total: 0, failed: 0, failedTests: [] };
|
|
3842
|
+
let testsAvailable = true;
|
|
3843
|
+
try {
|
|
3844
|
+
tests = await getTestSummary(context, cred, buildId);
|
|
3845
|
+
if (tests.failed > 0) {
|
|
3846
|
+
try {
|
|
3847
|
+
tests = { ...tests, failedTests: await getFailedTests(context, cred, buildId) };
|
|
3848
|
+
} catch {
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
} catch {
|
|
3852
|
+
testsAvailable = false;
|
|
3853
|
+
}
|
|
3854
|
+
return {
|
|
3855
|
+
id: build.id,
|
|
3856
|
+
name: build.buildNumber ?? null,
|
|
3857
|
+
state: mapBuildState(build.status),
|
|
3858
|
+
result: mapRunResult(build.result),
|
|
3859
|
+
// createdDate is the queue time, matching the run-list mapping.
|
|
3860
|
+
createdDate: build.queueTime ?? build.startTime ?? null,
|
|
3861
|
+
startedDate: build.startTime ?? null,
|
|
3862
|
+
finishedDate: build.finishTime ?? null,
|
|
3863
|
+
durationSeconds: secondsBetween(build.startTime, build.finishTime),
|
|
3864
|
+
reason: build.reason ?? null,
|
|
3865
|
+
requestedFor: build.requestedFor?.displayName ?? null,
|
|
3866
|
+
sourceBranch: build.sourceBranch ?? null,
|
|
3867
|
+
sourceCommit: build.sourceVersion ?? null,
|
|
3868
|
+
webUrl: build._links?.web?.href ?? null,
|
|
3869
|
+
errors,
|
|
3870
|
+
errorsAvailable,
|
|
3871
|
+
stages,
|
|
3872
|
+
jobs,
|
|
3873
|
+
tests,
|
|
3874
|
+
testsAvailable
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
async function getRunLogs(context, cred, buildId) {
|
|
3878
|
+
const url = withApiVersion(
|
|
3879
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs`)
|
|
3880
|
+
);
|
|
3881
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3882
|
+
const data = await readJsonResponse2(response);
|
|
3883
|
+
let logSteps = /* @__PURE__ */ new Map();
|
|
3884
|
+
try {
|
|
3885
|
+
logSteps = (await getBuildTimeline(context, cred, buildId)).logSteps;
|
|
3886
|
+
} catch {
|
|
3887
|
+
}
|
|
3888
|
+
return data.value.map((log) => ({
|
|
3889
|
+
id: log.id,
|
|
3890
|
+
createdOn: log.createdOn ?? null,
|
|
3891
|
+
lineCount: log.lineCount ?? null,
|
|
3892
|
+
step: logSteps.get(log.id) ?? null
|
|
3893
|
+
}));
|
|
3894
|
+
}
|
|
3895
|
+
async function getRunLog(context, cred, buildId, logId) {
|
|
3896
|
+
const url = withApiVersion(
|
|
3897
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs/${logId}`)
|
|
3898
|
+
);
|
|
3899
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3900
|
+
if (!response.ok) {
|
|
3901
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3902
|
+
}
|
|
3903
|
+
return response.text();
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
// src/commands/pipeline.ts
|
|
3907
|
+
var EXIT_FAILED = 1;
|
|
3908
|
+
var EXIT_CANCELED = 2;
|
|
3909
|
+
var EXIT_TIMEOUT = 124;
|
|
3373
3910
|
function writeError2(message) {
|
|
3374
3911
|
process.stderr.write(`Error: ${message}
|
|
3912
|
+
`);
|
|
3913
|
+
process.exitCode = 1;
|
|
3914
|
+
}
|
|
3915
|
+
function handlePipelineError(err, context) {
|
|
3916
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3917
|
+
if (error.message === "AUTH_FAILED") {
|
|
3918
|
+
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (error.message === "PERMISSION_DENIED") {
|
|
3922
|
+
writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
if (error.message === "NETWORK_ERROR") {
|
|
3926
|
+
writeError2("Could not connect to Azure DevOps. Check your network connection.");
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
if (error.message.startsWith("NOT_FOUND")) {
|
|
3930
|
+
writeError2(`Resource not found in ${context?.org}/${context?.project}.`);
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
if (error.message.startsWith("HTTP_")) {
|
|
3934
|
+
writeError2(`Azure DevOps request failed with ${error.message}.`);
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
writeError2(error.message);
|
|
3938
|
+
}
|
|
3939
|
+
function parsePositiveId(raw) {
|
|
3940
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
3941
|
+
const n = Number.parseInt(raw, 10);
|
|
3942
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
3943
|
+
}
|
|
3944
|
+
function parseOptionalCount(value, flag) {
|
|
3945
|
+
if (value === void 0) return void 0;
|
|
3946
|
+
const parsed = parsePositiveId(value);
|
|
3947
|
+
if (parsed === null) {
|
|
3948
|
+
writeError2(`Invalid ${flag} "${value}"; expected a positive integer.`);
|
|
3949
|
+
return null;
|
|
3950
|
+
}
|
|
3951
|
+
return parsed;
|
|
3952
|
+
}
|
|
3953
|
+
function formatBranchName2(refName) {
|
|
3954
|
+
if (!refName) return "\u2014";
|
|
3955
|
+
return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
|
|
3956
|
+
}
|
|
3957
|
+
async function resolvePipelineContext(options) {
|
|
3958
|
+
const context = resolveContext(options);
|
|
3959
|
+
const cred = await requireAuthCredential(context.org);
|
|
3960
|
+
return { context, cred };
|
|
3961
|
+
}
|
|
3962
|
+
function sleep(ms) {
|
|
3963
|
+
return new Promise((resolve2) => {
|
|
3964
|
+
setTimeout(resolve2, ms);
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
|
|
3968
|
+
const widths = [];
|
|
3969
|
+
for (const row of rows) {
|
|
3970
|
+
row.forEach((cell, i) => {
|
|
3971
|
+
widths[i] = Math.max(widths[i] ?? 0, cell.length);
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
return rows.map(
|
|
3975
|
+
(row) => row.map((cell, i) => rightAlign.has(i) ? cell.padStart(widths[i]) : cell.padEnd(widths[i])).join(" ").trimEnd()
|
|
3976
|
+
).join("\n");
|
|
3977
|
+
}
|
|
3978
|
+
function createPipelineListCommand() {
|
|
3979
|
+
const command = new Command13("list");
|
|
3980
|
+
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) => {
|
|
3981
|
+
validateOrgProjectPair(options);
|
|
3982
|
+
let context;
|
|
3983
|
+
try {
|
|
3984
|
+
const resolved = await resolvePipelineContext(options);
|
|
3985
|
+
context = resolved.context;
|
|
3986
|
+
let definitions = await getPipelineDefinitions(resolved.context, resolved.cred);
|
|
3987
|
+
if (options.filter) {
|
|
3988
|
+
const needle = options.filter.toLowerCase();
|
|
3989
|
+
definitions = definitions.filter((d) => d.name.toLowerCase().includes(needle));
|
|
3990
|
+
}
|
|
3991
|
+
if (options.json) {
|
|
3992
|
+
process.stdout.write(`${JSON.stringify(definitions, null, 2)}
|
|
3993
|
+
`);
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if (definitions.length === 0) {
|
|
3997
|
+
process.stdout.write("No pipelines found.\n");
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
const hasFolder = definitions.some((d) => d.folder);
|
|
4001
|
+
const rows = definitions.map(
|
|
4002
|
+
(d) => hasFolder ? [String(d.id), d.name, d.folder ?? ""] : [String(d.id), d.name]
|
|
4003
|
+
);
|
|
4004
|
+
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
4005
|
+
`);
|
|
4006
|
+
} catch (err) {
|
|
4007
|
+
handlePipelineError(err, context);
|
|
4008
|
+
}
|
|
4009
|
+
});
|
|
4010
|
+
return command;
|
|
4011
|
+
}
|
|
4012
|
+
function runRow(run) {
|
|
4013
|
+
const status2 = run.result ? `${run.state}/${run.result}` : run.state;
|
|
4014
|
+
return [
|
|
4015
|
+
String(run.id),
|
|
4016
|
+
`[${status2}]`,
|
|
4017
|
+
run.createdDate ?? "\u2014",
|
|
4018
|
+
formatBranchName2(run.sourceBranch),
|
|
4019
|
+
run.sourceCommit ? run.sourceCommit.slice(0, 8) : "\u2014"
|
|
4020
|
+
];
|
|
4021
|
+
}
|
|
4022
|
+
var COMMIT_SHA_PATTERN = /^[0-9a-f]{6,40}$/i;
|
|
4023
|
+
function parseGetRunsInputs(defIdRaw, options) {
|
|
4024
|
+
let defId;
|
|
4025
|
+
if (defIdRaw !== void 0) {
|
|
4026
|
+
const parsed = parsePositiveId(defIdRaw);
|
|
4027
|
+
if (parsed === null) {
|
|
4028
|
+
writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
|
|
4029
|
+
return null;
|
|
4030
|
+
}
|
|
4031
|
+
defId = parsed;
|
|
4032
|
+
} else if (options.commit === void 0 && options.pr === void 0) {
|
|
4033
|
+
writeError2("Definition id is required unless --commit or --pr is given.");
|
|
4034
|
+
return null;
|
|
4035
|
+
}
|
|
4036
|
+
const limit = parseOptionalCount(options.limit, "--limit");
|
|
4037
|
+
if (limit === null) return null;
|
|
4038
|
+
const prNumber = parseOptionalCount(options.pr, "--pr");
|
|
4039
|
+
if (prNumber === null) return null;
|
|
4040
|
+
if (options.commit !== void 0 && !COMMIT_SHA_PATTERN.test(options.commit)) {
|
|
4041
|
+
writeError2(`Invalid --commit "${options.commit}"; expected 6-40 hex characters.`);
|
|
4042
|
+
return null;
|
|
4043
|
+
}
|
|
4044
|
+
if (options.branch !== void 0 && prNumber !== void 0) {
|
|
4045
|
+
writeError2("Use either --branch or --pr, not both.");
|
|
4046
|
+
return null;
|
|
4047
|
+
}
|
|
4048
|
+
return { defId, limit: limit ?? 10, prNumber };
|
|
4049
|
+
}
|
|
4050
|
+
function createPipelineGetRunsCommand() {
|
|
4051
|
+
const command = new Command13("get-runs");
|
|
4052
|
+
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) => {
|
|
4053
|
+
validateOrgProjectPair(options);
|
|
4054
|
+
const inputs = parseGetRunsInputs(defIdRaw, options);
|
|
4055
|
+
if (inputs === null) {
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
let context;
|
|
4059
|
+
try {
|
|
4060
|
+
const resolved = await resolvePipelineContext(options);
|
|
4061
|
+
context = resolved.context;
|
|
4062
|
+
const runs = await getPipelineRuns(resolved.context, resolved.cred, {
|
|
4063
|
+
definitionId: inputs.defId,
|
|
4064
|
+
branch: options.branch,
|
|
4065
|
+
prNumber: inputs.prNumber,
|
|
4066
|
+
commit: options.commit,
|
|
4067
|
+
top: inputs.limit
|
|
4068
|
+
});
|
|
4069
|
+
if (options.json) {
|
|
4070
|
+
process.stdout.write(`${JSON.stringify(runs, null, 2)}
|
|
4071
|
+
`);
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
if (runs.length === 0) {
|
|
4075
|
+
process.stdout.write(
|
|
4076
|
+
inputs.defId === void 0 ? "No runs found matching the filters.\n" : `No runs found for pipeline ${inputs.defId}.
|
|
4077
|
+
`
|
|
4078
|
+
);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
process.stdout.write(`${formatTable(runs.map(runRow), /* @__PURE__ */ new Set([0]))}
|
|
4082
|
+
`);
|
|
4083
|
+
} catch (err) {
|
|
4084
|
+
handlePipelineError(err, context);
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
return command;
|
|
4088
|
+
}
|
|
4089
|
+
function applyWaitExitCode(result) {
|
|
4090
|
+
if (result.timedOut) {
|
|
4091
|
+
process.exitCode = EXIT_TIMEOUT;
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
switch (result.result) {
|
|
4095
|
+
case "succeeded":
|
|
4096
|
+
return;
|
|
4097
|
+
case "canceled":
|
|
4098
|
+
process.exitCode = EXIT_CANCELED;
|
|
4099
|
+
return;
|
|
4100
|
+
case "failed":
|
|
4101
|
+
default:
|
|
4102
|
+
process.exitCode = EXIT_FAILED;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
function createPipelineWaitCommand() {
|
|
4106
|
+
const command = new Command13("wait");
|
|
4107
|
+
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) => {
|
|
4108
|
+
validateOrgProjectPair(options);
|
|
4109
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4110
|
+
if (runId === null) {
|
|
4111
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
const timeoutSec = options.timeout === void 0 ? 1800 : Number(options.timeout);
|
|
4115
|
+
const pollSec = options.pollInterval === void 0 ? 5 : Number(options.pollInterval);
|
|
4116
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
|
|
4117
|
+
writeError2(`Invalid --timeout "${options.timeout}"; expected a non-negative number.`);
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
if (!Number.isFinite(pollSec) || pollSec <= 0) {
|
|
4121
|
+
writeError2(`Invalid --poll-interval "${options.pollInterval}"; expected a positive number.`);
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
let context;
|
|
4125
|
+
try {
|
|
4126
|
+
const resolved = await resolvePipelineContext(options);
|
|
4127
|
+
context = resolved.context;
|
|
4128
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
4129
|
+
let waitResult = null;
|
|
4130
|
+
for (; ; ) {
|
|
4131
|
+
const status2 = await getBuildStatus(resolved.context, resolved.cred, runId);
|
|
4132
|
+
if (status2.state === "completed") {
|
|
4133
|
+
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
if (Date.now() >= deadline) {
|
|
4137
|
+
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
4138
|
+
break;
|
|
4139
|
+
}
|
|
4140
|
+
await sleep(pollSec * 1e3);
|
|
4141
|
+
}
|
|
4142
|
+
applyWaitExitCode(waitResult);
|
|
4143
|
+
if (options.json) {
|
|
4144
|
+
process.stdout.write(`${JSON.stringify(waitResult, null, 2)}
|
|
4145
|
+
`);
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
if (waitResult.timedOut) {
|
|
4149
|
+
process.stdout.write(`Run ${runId} did not finish within ${timeoutSec}s (still ${waitResult.state}).
|
|
4150
|
+
`);
|
|
4151
|
+
} else {
|
|
4152
|
+
process.stdout.write(`Run ${runId} finished: ${waitResult.result ?? waitResult.state}.
|
|
4153
|
+
`);
|
|
4154
|
+
}
|
|
4155
|
+
} catch (err) {
|
|
4156
|
+
handlePipelineError(err, context);
|
|
4157
|
+
}
|
|
4158
|
+
});
|
|
4159
|
+
return command;
|
|
4160
|
+
}
|
|
4161
|
+
function timelineRows(items, available) {
|
|
4162
|
+
if (!available) {
|
|
4163
|
+
return [" unavailable"];
|
|
4164
|
+
}
|
|
4165
|
+
if (items.length === 0) {
|
|
4166
|
+
return [" (none)"];
|
|
4167
|
+
}
|
|
4168
|
+
return items.map((item) => ` - ${item.name} [${item.result ?? item.state}]`);
|
|
4169
|
+
}
|
|
4170
|
+
function formatDuration(totalSeconds) {
|
|
4171
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
4172
|
+
const m = Math.floor(totalSeconds % 3600 / 60);
|
|
4173
|
+
const s = totalSeconds % 60;
|
|
4174
|
+
if (h > 0) return `${h}h${m}m${s}s`;
|
|
4175
|
+
if (m > 0) return `${m}m${s}s`;
|
|
4176
|
+
return `${s}s`;
|
|
4177
|
+
}
|
|
4178
|
+
function errorRows(detail) {
|
|
4179
|
+
if (!detail.errorsAvailable) {
|
|
4180
|
+
return [" unavailable"];
|
|
4181
|
+
}
|
|
4182
|
+
if (detail.errors.length === 0) {
|
|
4183
|
+
return [" (none)"];
|
|
4184
|
+
}
|
|
4185
|
+
return detail.errors.map((error) => {
|
|
4186
|
+
const source = error.source ? `[${error.source}] ` : "";
|
|
4187
|
+
return ` - ${source}${error.message}`;
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
function failedTestRow(test) {
|
|
4191
|
+
if (!test.errorMessage) {
|
|
4192
|
+
return ` - ${test.name}`;
|
|
4193
|
+
}
|
|
4194
|
+
const firstLine = test.errorMessage.split("\n", 1)[0].trim();
|
|
4195
|
+
return ` - ${test.name}: ${firstLine}`;
|
|
4196
|
+
}
|
|
4197
|
+
function testRows(detail) {
|
|
4198
|
+
if (!detail.testsAvailable) {
|
|
4199
|
+
return [" unavailable"];
|
|
4200
|
+
}
|
|
4201
|
+
if (detail.tests.present) {
|
|
4202
|
+
return [
|
|
4203
|
+
` ${detail.tests.failed} failing of ${detail.tests.total}`,
|
|
4204
|
+
...detail.tests.failedTests.map(failedTestRow)
|
|
4205
|
+
];
|
|
4206
|
+
}
|
|
4207
|
+
return [" no tests present"];
|
|
4208
|
+
}
|
|
4209
|
+
function formatRunDetail(detail) {
|
|
4210
|
+
const status2 = detail.result ? `${detail.state}/${detail.result}` : detail.state;
|
|
4211
|
+
const name = detail.name ? ` ${detail.name}` : "";
|
|
4212
|
+
const duration = detail.durationSeconds == null ? "\u2014" : formatDuration(detail.durationSeconds);
|
|
4213
|
+
return [
|
|
4214
|
+
`Run #${detail.id} [${status2}]${name}`,
|
|
4215
|
+
`Queued: ${detail.createdDate ?? "\u2014"} Started: ${detail.startedDate ?? "\u2014"} Finished: ${detail.finishedDate ?? "\u2014"}`,
|
|
4216
|
+
`Duration: ${duration} Reason: ${detail.reason ?? "\u2014"} Requested for: ${detail.requestedFor ?? "\u2014"}`,
|
|
4217
|
+
`Branch: ${formatBranchName2(detail.sourceBranch)} Commit: ${detail.sourceCommit ?? "unavailable"}`,
|
|
4218
|
+
...detail.webUrl ? [`Link: ${detail.webUrl}`] : [],
|
|
4219
|
+
"",
|
|
4220
|
+
"Stages:",
|
|
4221
|
+
...timelineRows(detail.stages, detail.errorsAvailable),
|
|
4222
|
+
"",
|
|
4223
|
+
"Jobs:",
|
|
4224
|
+
...timelineRows(detail.jobs, detail.errorsAvailable),
|
|
4225
|
+
"",
|
|
4226
|
+
"Errors:",
|
|
4227
|
+
...errorRows(detail),
|
|
4228
|
+
"",
|
|
4229
|
+
"Tests:",
|
|
4230
|
+
...testRows(detail)
|
|
4231
|
+
].join("\n");
|
|
4232
|
+
}
|
|
4233
|
+
function createPipelineGetRunDetailCommand() {
|
|
4234
|
+
const command = new Command13("get-run-detail");
|
|
4235
|
+
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(async (runIdRaw, options) => {
|
|
4236
|
+
validateOrgProjectPair(options);
|
|
4237
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4238
|
+
if (runId === null) {
|
|
4239
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
let context;
|
|
4243
|
+
try {
|
|
4244
|
+
const resolved = await resolvePipelineContext(options);
|
|
4245
|
+
context = resolved.context;
|
|
4246
|
+
const detail = await getRunDetail(resolved.context, resolved.cred, runId);
|
|
4247
|
+
if (options.json) {
|
|
4248
|
+
process.stdout.write(`${JSON.stringify(detail, null, 2)}
|
|
4249
|
+
`);
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
process.stdout.write(`${formatRunDetail(detail)}
|
|
4253
|
+
`);
|
|
4254
|
+
} catch (err) {
|
|
4255
|
+
handlePipelineError(err, context);
|
|
4256
|
+
}
|
|
4257
|
+
});
|
|
4258
|
+
return command;
|
|
4259
|
+
}
|
|
4260
|
+
function grepWithContext(lines, grep, context) {
|
|
4261
|
+
const include = /* @__PURE__ */ new Set();
|
|
4262
|
+
lines.forEach((line, i) => {
|
|
4263
|
+
if (grep.test(line)) {
|
|
4264
|
+
for (let j = Math.max(0, i - context); j <= Math.min(lines.length - 1, i + context); j++) {
|
|
4265
|
+
include.add(j);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
});
|
|
4269
|
+
const selected = [];
|
|
4270
|
+
let prev = -1;
|
|
4271
|
+
for (const i of [...include].sort((a, b) => a - b)) {
|
|
4272
|
+
if (selected.length > 0 && i > prev + 1) {
|
|
4273
|
+
selected.push("--");
|
|
4274
|
+
}
|
|
4275
|
+
selected.push(lines[i]);
|
|
4276
|
+
prev = i;
|
|
4277
|
+
}
|
|
4278
|
+
return selected;
|
|
4279
|
+
}
|
|
4280
|
+
function filterLogLines(content, grep, tail, context) {
|
|
4281
|
+
let lines = content.split("\n");
|
|
4282
|
+
if (lines.at(-1) === "") {
|
|
4283
|
+
lines.pop();
|
|
4284
|
+
}
|
|
4285
|
+
if (grep) {
|
|
4286
|
+
lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
|
|
4287
|
+
}
|
|
4288
|
+
if (tail !== void 0 && lines.length > tail) {
|
|
4289
|
+
lines = lines.slice(-tail);
|
|
4290
|
+
}
|
|
4291
|
+
return lines;
|
|
4292
|
+
}
|
|
4293
|
+
function parseLogFilters(options) {
|
|
4294
|
+
if (options.logId !== void 0 && options.step !== void 0) {
|
|
4295
|
+
writeError2("Use either --log-id or --step, not both.");
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
|
|
4299
|
+
const slices = options.tail !== void 0 || options.grep !== void 0 || options.context !== void 0;
|
|
4300
|
+
if (slices && !selectsSingleLog) {
|
|
4301
|
+
writeError2("--tail, --grep, and --context require --log-id or --step.");
|
|
4302
|
+
return null;
|
|
4303
|
+
}
|
|
4304
|
+
if (options.context !== void 0 && options.grep === void 0) {
|
|
4305
|
+
writeError2("--context requires --grep.");
|
|
4306
|
+
return null;
|
|
4307
|
+
}
|
|
4308
|
+
const tail = parseOptionalCount(options.tail, "--tail");
|
|
4309
|
+
if (tail === null) return null;
|
|
4310
|
+
const contextLines = parseOptionalCount(options.context, "--context");
|
|
4311
|
+
if (contextLines === null) return null;
|
|
4312
|
+
let grep;
|
|
4313
|
+
if (options.grep !== void 0) {
|
|
4314
|
+
try {
|
|
4315
|
+
grep = new RegExp(options.grep);
|
|
4316
|
+
} catch {
|
|
4317
|
+
writeError2(`Invalid --grep "${options.grep}"; expected a valid regular expression.`);
|
|
4318
|
+
return null;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return { tail, contextLines: contextLines ?? 0, grep };
|
|
4322
|
+
}
|
|
4323
|
+
function chooseStepLog(logs, step, runId) {
|
|
4324
|
+
const needle = step.toLowerCase();
|
|
4325
|
+
const matches = logs.filter((l) => l.step?.toLowerCase().includes(needle));
|
|
4326
|
+
const exact = matches.filter((l) => l.step?.toLowerCase() === needle);
|
|
4327
|
+
const chosen = exact.length === 1 ? exact : matches;
|
|
4328
|
+
if (chosen.length === 0) {
|
|
4329
|
+
writeError2(`No log matches step "${step}" in run ${runId}.`);
|
|
4330
|
+
return null;
|
|
4331
|
+
}
|
|
4332
|
+
if (chosen.length > 1) {
|
|
4333
|
+
const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
|
|
4334
|
+
writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
4337
|
+
return chosen[0].id;
|
|
4338
|
+
}
|
|
4339
|
+
async function resolveRequestedLogId(resolved, runId, options) {
|
|
4340
|
+
if (options.logId !== void 0) {
|
|
4341
|
+
const parsed = parsePositiveId(options.logId);
|
|
4342
|
+
if (parsed === null) {
|
|
4343
|
+
writeError2(`Invalid --log-id "${options.logId}"; expected a positive integer.`);
|
|
4344
|
+
return null;
|
|
4345
|
+
}
|
|
4346
|
+
return parsed;
|
|
4347
|
+
}
|
|
4348
|
+
if (options.step === void 0) {
|
|
4349
|
+
return void 0;
|
|
4350
|
+
}
|
|
4351
|
+
const allLogs = await getRunLogs(resolved.context, resolved.cred, runId);
|
|
4352
|
+
return chooseStepLog(allLogs, options.step, runId);
|
|
4353
|
+
}
|
|
4354
|
+
function printSingleLog(content, filters) {
|
|
4355
|
+
if (filters.grep !== void 0 || filters.tail !== void 0) {
|
|
4356
|
+
const lines = filterLogLines(content, filters.grep, filters.tail, filters.contextLines);
|
|
4357
|
+
if (lines.length > 0) {
|
|
4358
|
+
process.stdout.write(`${lines.join("\n")}
|
|
4359
|
+
`);
|
|
4360
|
+
}
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
process.stdout.write(content.endsWith("\n") ? content : `${content}
|
|
4364
|
+
`);
|
|
4365
|
+
}
|
|
4366
|
+
function createPipelineLogsCommand() {
|
|
4367
|
+
const command = new Command13("logs");
|
|
4368
|
+
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) => {
|
|
4369
|
+
validateOrgProjectPair(options);
|
|
4370
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4371
|
+
if (runId === null) {
|
|
4372
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4375
|
+
const filters = parseLogFilters(options);
|
|
4376
|
+
if (filters === null) {
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
let context;
|
|
4380
|
+
try {
|
|
4381
|
+
const resolved = await resolvePipelineContext(options);
|
|
4382
|
+
context = resolved.context;
|
|
4383
|
+
const logId = await resolveRequestedLogId(resolved, runId, options);
|
|
4384
|
+
if (logId === null) {
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
if (logId !== void 0) {
|
|
4388
|
+
const content = await getRunLog(resolved.context, resolved.cred, runId, logId);
|
|
4389
|
+
printSingleLog(content, filters);
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
const logs = await getRunLogs(resolved.context, resolved.cred, runId);
|
|
4393
|
+
if (options.json) {
|
|
4394
|
+
process.stdout.write(`${JSON.stringify(logs, null, 2)}
|
|
4395
|
+
`);
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
if (logs.length === 0) {
|
|
4399
|
+
process.stdout.write(`No logs found for run ${runId}.
|
|
4400
|
+
`);
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
const rows = logs.map((l) => [
|
|
4404
|
+
String(l.id),
|
|
4405
|
+
l.createdOn ?? "\u2014",
|
|
4406
|
+
l.lineCount == null ? "" : `${l.lineCount} lines`,
|
|
4407
|
+
l.step ?? ""
|
|
4408
|
+
]);
|
|
4409
|
+
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
4410
|
+
`);
|
|
4411
|
+
} catch (err) {
|
|
4412
|
+
handlePipelineError(err, context);
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
4415
|
+
return command;
|
|
4416
|
+
}
|
|
4417
|
+
function createPipelineTestsCommand() {
|
|
4418
|
+
const command = new Command13("tests");
|
|
4419
|
+
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) => {
|
|
4420
|
+
validateOrgProjectPair(options);
|
|
4421
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4422
|
+
if (runId === null) {
|
|
4423
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
let context;
|
|
4427
|
+
try {
|
|
4428
|
+
const resolved = await resolvePipelineContext(options);
|
|
4429
|
+
context = resolved.context;
|
|
4430
|
+
const summary = await getTestSummary(resolved.context, resolved.cred, runId);
|
|
4431
|
+
const failedTests = summary.failed > 0 ? await getFailedTests(resolved.context, resolved.cred, runId) : [];
|
|
4432
|
+
if (options.json) {
|
|
4433
|
+
process.stdout.write(`${JSON.stringify({ ...summary, failedTests }, null, 2)}
|
|
4434
|
+
`);
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
if (!summary.present) {
|
|
4438
|
+
process.stdout.write(`No test results published for run ${runId}.
|
|
4439
|
+
`);
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
if (!options.failed) {
|
|
4443
|
+
process.stdout.write(`Run #${runId}: ${summary.failed} failing of ${summary.total} tests
|
|
4444
|
+
`);
|
|
4445
|
+
}
|
|
4446
|
+
if (failedTests.length > 0) {
|
|
4447
|
+
process.stdout.write(`${failedTests.map(failedTestRow).join("\n")}
|
|
4448
|
+
`);
|
|
4449
|
+
} else if (options.failed) {
|
|
4450
|
+
process.stdout.write("No failing tests.\n");
|
|
4451
|
+
}
|
|
4452
|
+
} catch (err) {
|
|
4453
|
+
handlePipelineError(err, context);
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
return command;
|
|
4457
|
+
}
|
|
4458
|
+
function parseParameters(values) {
|
|
4459
|
+
const result = {};
|
|
4460
|
+
for (const entry of values ?? []) {
|
|
4461
|
+
const eq = entry.indexOf("=");
|
|
4462
|
+
if (eq <= 0) {
|
|
4463
|
+
return null;
|
|
4464
|
+
}
|
|
4465
|
+
const key = entry.slice(0, eq);
|
|
4466
|
+
const value = entry.slice(eq + 1);
|
|
4467
|
+
result[key] = value;
|
|
4468
|
+
}
|
|
4469
|
+
return result;
|
|
4470
|
+
}
|
|
4471
|
+
function collectParameter(value, previous) {
|
|
4472
|
+
return previous.concat([value]);
|
|
4473
|
+
}
|
|
4474
|
+
function createPipelineStartCommand() {
|
|
4475
|
+
const command = new Command13("start");
|
|
4476
|
+
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) => {
|
|
4477
|
+
validateOrgProjectPair(options);
|
|
4478
|
+
const defId = parsePositiveId(defIdRaw);
|
|
4479
|
+
if (defId === null) {
|
|
4480
|
+
writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
const parameters = parseParameters(options.parameter);
|
|
4484
|
+
if (parameters === null) {
|
|
4485
|
+
writeError2("Invalid --parameter; expected key=value.");
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
let context;
|
|
4489
|
+
try {
|
|
4490
|
+
const resolved = await resolvePipelineContext(options);
|
|
4491
|
+
context = resolved.context;
|
|
4492
|
+
const result = await runPipeline(resolved.context, resolved.cred, defId, {
|
|
4493
|
+
branch: options.branch,
|
|
4494
|
+
parameters
|
|
4495
|
+
});
|
|
4496
|
+
if (options.json) {
|
|
4497
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4498
|
+
`);
|
|
4499
|
+
return;
|
|
4500
|
+
}
|
|
4501
|
+
process.stdout.write(`Queued run #${result.id} [${result.state}]
|
|
4502
|
+
${result.webUrl ?? "\u2014"}
|
|
4503
|
+
`);
|
|
4504
|
+
} catch (err) {
|
|
4505
|
+
handlePipelineError(err, context);
|
|
4506
|
+
}
|
|
4507
|
+
});
|
|
4508
|
+
return command;
|
|
4509
|
+
}
|
|
4510
|
+
function createPipelineCommand() {
|
|
4511
|
+
const command = new Command13("pipeline");
|
|
4512
|
+
command.description("Manage Azure DevOps pipelines");
|
|
4513
|
+
command.addCommand(createPipelineListCommand());
|
|
4514
|
+
command.addCommand(createPipelineGetRunsCommand());
|
|
4515
|
+
command.addCommand(createPipelineWaitCommand());
|
|
4516
|
+
command.addCommand(createPipelineGetRunDetailCommand());
|
|
4517
|
+
command.addCommand(createPipelineLogsCommand());
|
|
4518
|
+
command.addCommand(createPipelineTestsCommand());
|
|
4519
|
+
command.addCommand(createPipelineStartCommand());
|
|
4520
|
+
return command;
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
// src/commands/comments.ts
|
|
4524
|
+
import { Command as Command14 } from "commander";
|
|
4525
|
+
function writeError3(message) {
|
|
4526
|
+
process.stderr.write(`Error: ${message}
|
|
3375
4527
|
`);
|
|
3376
4528
|
process.exit(1);
|
|
3377
4529
|
}
|
|
@@ -3389,7 +4541,7 @@ function formatComments(result, convertMarkdown) {
|
|
|
3389
4541
|
return lines.join("\n");
|
|
3390
4542
|
}
|
|
3391
4543
|
function createCommentsListCommand() {
|
|
3392
|
-
const command = new
|
|
4544
|
+
const command = new Command14("list");
|
|
3393
4545
|
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) => {
|
|
3394
4546
|
validateOrgProjectPair(options);
|
|
3395
4547
|
const id = parseWorkItemId(idStr);
|
|
@@ -3417,12 +4569,12 @@ function createCommentsListCommand() {
|
|
|
3417
4569
|
return command;
|
|
3418
4570
|
}
|
|
3419
4571
|
function createCommentsAddCommand() {
|
|
3420
|
-
const command = new
|
|
4572
|
+
const command = new Command14("add");
|
|
3421
4573
|
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) => {
|
|
3422
4574
|
validateOrgProjectPair(options);
|
|
3423
4575
|
const id = parseWorkItemId(idStr);
|
|
3424
4576
|
if (text.trim() === "") {
|
|
3425
|
-
|
|
4577
|
+
writeError3("Comment text must be a non-empty string.");
|
|
3426
4578
|
}
|
|
3427
4579
|
let context;
|
|
3428
4580
|
try {
|
|
@@ -3444,7 +4596,7 @@ function createCommentsAddCommand() {
|
|
|
3444
4596
|
return command;
|
|
3445
4597
|
}
|
|
3446
4598
|
function createCommentsCommand() {
|
|
3447
|
-
const command = new
|
|
4599
|
+
const command = new Command14("comments");
|
|
3448
4600
|
command.description("Manage Azure DevOps work item comments");
|
|
3449
4601
|
command.addCommand(createCommentsListCommand());
|
|
3450
4602
|
command.addCommand(createCommentsAddCommand());
|
|
@@ -3452,12 +4604,12 @@ function createCommentsCommand() {
|
|
|
3452
4604
|
}
|
|
3453
4605
|
|
|
3454
4606
|
// src/commands/download-attachment.ts
|
|
3455
|
-
import { Command as
|
|
4607
|
+
import { Command as Command15 } from "commander";
|
|
3456
4608
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
3457
4609
|
import { existsSync as existsSync5 } from "fs";
|
|
3458
4610
|
import { join as join3 } from "path";
|
|
3459
4611
|
function createDownloadAttachmentCommand() {
|
|
3460
|
-
const command = new
|
|
4612
|
+
const command = new Command15("download-attachment");
|
|
3461
4613
|
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(
|
|
3462
4614
|
async (idStr, filename, options) => {
|
|
3463
4615
|
const id = parseWorkItemId(idStr);
|
|
@@ -3499,18 +4651,18 @@ function createDownloadAttachmentCommand() {
|
|
|
3499
4651
|
}
|
|
3500
4652
|
|
|
3501
4653
|
// src/services/update-check.ts
|
|
3502
|
-
import
|
|
3503
|
-
import
|
|
4654
|
+
import fs2 from "fs";
|
|
4655
|
+
import path2 from "path";
|
|
3504
4656
|
import os from "os";
|
|
3505
4657
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
3506
4658
|
var FETCH_TIMEOUT_MS = 1500;
|
|
3507
4659
|
var REGISTRY_URL = "https://registry.npmjs.org/azdo-cli/latest";
|
|
3508
4660
|
function getCachePath() {
|
|
3509
|
-
return
|
|
4661
|
+
return path2.join(os.homedir(), ".azdo", "update-check.json");
|
|
3510
4662
|
}
|
|
3511
4663
|
function defaultReadCache() {
|
|
3512
4664
|
try {
|
|
3513
|
-
return
|
|
4665
|
+
return fs2.readFileSync(getCachePath(), "utf-8");
|
|
3514
4666
|
} catch {
|
|
3515
4667
|
return null;
|
|
3516
4668
|
}
|
|
@@ -3518,8 +4670,8 @@ function defaultReadCache() {
|
|
|
3518
4670
|
function defaultWriteCache(data) {
|
|
3519
4671
|
try {
|
|
3520
4672
|
const cachePath = getCachePath();
|
|
3521
|
-
|
|
3522
|
-
|
|
4673
|
+
fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
|
|
4674
|
+
fs2.writeFileSync(cachePath, data);
|
|
3523
4675
|
} catch {
|
|
3524
4676
|
}
|
|
3525
4677
|
}
|
|
@@ -3603,7 +4755,15 @@ async function getUpdateNotice(opts) {
|
|
|
3603
4755
|
}
|
|
3604
4756
|
|
|
3605
4757
|
// src/index.ts
|
|
3606
|
-
|
|
4758
|
+
function exitOnEpipe(err) {
|
|
4759
|
+
if (err.code === "EPIPE") {
|
|
4760
|
+
process.exit(0);
|
|
4761
|
+
}
|
|
4762
|
+
throw err;
|
|
4763
|
+
}
|
|
4764
|
+
process.stdout.on("error", exitOnEpipe);
|
|
4765
|
+
process.stderr.on("error", exitOnEpipe);
|
|
4766
|
+
var program = new Command16();
|
|
3607
4767
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
3608
4768
|
program.option("--no-update-check", "Skip the check for a newer published version");
|
|
3609
4769
|
program.addCommand(createGetItemCommand());
|
|
@@ -3618,6 +4778,7 @@ program.addCommand(createSetMdFieldCommand());
|
|
|
3618
4778
|
program.addCommand(createUpsertCommand());
|
|
3619
4779
|
program.addCommand(createListFieldsCommand());
|
|
3620
4780
|
program.addCommand(createPrCommand());
|
|
4781
|
+
program.addCommand(createPipelineCommand());
|
|
3621
4782
|
program.addCommand(createCommentsCommand());
|
|
3622
4783
|
program.addCommand(createDownloadAttachmentCommand());
|
|
3623
4784
|
program.showHelpAfterError();
|