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
- } from "./chunk-C7RAZJHV.js";
36
+ unsetConfigValue,
37
+ unsetOrgScopedValue
38
+ } from "./chunk-XVXMDWQE.js";
32
39
 
33
40
  // src/index.ts
34
- import { Command as Command15 } from "commander";
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 getWorkItem(context, id, cred, extraFields) {
249
- const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
250
- const data = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, {
251
- fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
252
- }) : await fetchWorkItemResponse(context, id, cred, { includeRelations: true });
253
- const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, { includeRelations: true }) : data;
254
- const descriptionParts = [];
255
- if (data.fields["System.Description"]) {
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
- if (data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
259
- descriptionParts.push({ label: "Acceptance Criteria", value: data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
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 (data.fields["Microsoft.VSTS.TCM.ReproSteps"]) {
262
- descriptionParts.push({ label: "Repro Steps", value: data.fields["Microsoft.VSTS.TCM.ReproSteps"] });
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
- let combinedDescription = null;
265
- if (descriptionParts.length === 1) {
266
- combinedDescription = descriptionParts.at(0)?.value ?? null;
267
- } else if (descriptionParts.length > 1) {
268
- combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
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: combinedDescription,
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: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null,
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 sleep = deps.sleep ?? defaultSleep;
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 sleep(intervalSec * 1e3);
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 { execSync } from "child_process";
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 noticeCredentialBearingRemote() {
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(WARNING);
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 httpsUserinfo = /^https?:\/\/[^@/]+@/;
798
- function parseAzdoRemote(url) {
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
- if (httpsUserinfo.test(url)) {
803
- noticeCredentialBearingRemote();
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
- const project = match[2];
806
- if (/^DefaultCollection$/i.test(project)) {
807
- return { org: match[1], project: "" };
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 null;
967
+ return lines.join("\n");
813
968
  }
814
969
  function detectAzdoContext() {
815
- let remoteUrl;
970
+ let configContent;
816
971
  try {
817
- remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
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 context = parseAzdoRemote(remoteUrl);
822
- if (!context || !context.org && !context.project) {
823
- throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
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 context;
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 (httpsUserinfo.test(url)) {
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 = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
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 = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
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 whose origin remote is an Azure DevOps URL.",
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 project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || config.project;
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 fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
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 ?? loadConfig().markdown ?? false;
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
- setConfigValue(key, value);
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
- process.stdout.write(`Set "${key}" to "${value}"
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
- process.stdout.write(JSON.stringify(cfg) + "\n");
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
- unsetConfigValue(key);
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
- process.stdout.write(`Unset "${key}"
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/comments.ts
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 Command13("list");
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 Command13("add");
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
- writeError2("Comment text must be a non-empty string.");
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 Command13("comments");
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 Command14 } from "commander";
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 Command14("download-attachment");
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 fs from "fs";
3503
- import path from "path";
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 path.join(os.homedir(), ".azdo", "update-check.json");
4661
+ return path2.join(os.homedir(), ".azdo", "update-check.json");
3510
4662
  }
3511
4663
  function defaultReadCache() {
3512
4664
  try {
3513
- return fs.readFileSync(getCachePath(), "utf-8");
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
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3522
- fs.writeFileSync(cachePath, data);
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
- var program = new Command15();
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();