azdo-cli 0.22.0-develop.798 → 0.22.0-develop.822

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +311 -45
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -139,6 +139,9 @@ azdo pipeline get-runs 12 --branch develop --limit 1
139
139
  azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
140
140
  azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
141
141
  azdo pipeline start 12 --branch develop --parameter env=staging
142
+ azdo pipeline artifacts 3456 # list a run's build artifacts (name, type, size)
143
+ azdo pipeline artifact-download 3456 reports --path ./out # extract into ./out (--all for every artifact, --force to overwrite)
144
+ azdo pipeline logs 3456 --step "Trivy" --head 50 --no-progress # also --tail/--grep; --no-progress drops CR redraws
142
145
 
143
146
  # Work item relations — types, add, remove, list
144
147
  azdo relations types # list all relation types (Child, Parent, Related, ...)
package/dist/index.js CHANGED
@@ -4501,11 +4501,11 @@ function branchTemplateSegments(branch) {
4501
4501
  }
4502
4502
  return segments;
4503
4503
  }
4504
- async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
4504
+ async function fetchRepositoryItemContent(context, repo, cred, path5, branch) {
4505
4505
  const url = new URL(
4506
4506
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/items`
4507
4507
  );
4508
- url.searchParams.set("path", path3);
4508
+ url.searchParams.set("path", path5);
4509
4509
  url.searchParams.set("versionDescriptor.version", branch);
4510
4510
  url.searchParams.set("versionDescriptor.versionType", "branch");
4511
4511
  url.searchParams.set("includeContent", "true");
@@ -4548,10 +4548,10 @@ function defaultTemplateCandidates() {
4548
4548
  return paths;
4549
4549
  }
4550
4550
  async function firstMatchingTemplate(context, repo, cred, defaultBranch, candidates, kind) {
4551
- for (const path3 of candidates) {
4552
- const content = await fetchRepositoryItemContent(context, repo, cred, path3, defaultBranch);
4551
+ for (const path5 of candidates) {
4552
+ const content = await fetchRepositoryItemContent(context, repo, cred, path5, defaultBranch);
4553
4553
  if (content !== null) {
4554
- return { path: path3, content, kind };
4554
+ return { path: path5, content, kind };
4555
4555
  }
4556
4556
  }
4557
4557
  return null;
@@ -6348,6 +6348,7 @@ function createPrCommand() {
6348
6348
 
6349
6349
  // src/commands/pipeline.ts
6350
6350
  import { Command as Command14 } from "commander";
6351
+ import path3 from "path";
6351
6352
 
6352
6353
  // src/services/pipeline-client.ts
6353
6354
  var API_VERSION = "7.1";
@@ -6471,6 +6472,9 @@ async function getBuildStatus(context, cred, buildId) {
6471
6472
  const build = await getBuild(context, cred, buildId);
6472
6473
  return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
6473
6474
  }
6475
+ function recordErrors(record) {
6476
+ return (record.issues ?? []).filter((issue) => issue.type === "error" && issue.message).map((issue) => ({ message: issue.message ?? "", source: record.name ?? null }));
6477
+ }
6474
6478
  async function getBuildTimeline(context, cred, buildId) {
6475
6479
  const url = withApiVersion(
6476
6480
  new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
@@ -6482,14 +6486,20 @@ async function getBuildTimeline(context, cred, buildId) {
6482
6486
  const stages = [];
6483
6487
  const jobs = [];
6484
6488
  const logSteps = /* @__PURE__ */ new Map();
6489
+ const logRecords = /* @__PURE__ */ new Map();
6490
+ const namesById = /* @__PURE__ */ new Map();
6485
6491
  for (const record of records) {
6486
- for (const issue of record.issues ?? []) {
6487
- if (issue.type === "error" && issue.message) {
6488
- errors.push({ message: issue.message, source: record.name ?? null });
6489
- }
6490
- }
6492
+ if (record.id && record.name) namesById.set(record.id, record.name);
6493
+ }
6494
+ for (const record of records) {
6495
+ errors.push(...recordErrors(record));
6491
6496
  if (record.name && record.log?.id !== void 0) {
6492
6497
  logSteps.set(record.log.id, record.name);
6498
+ logRecords.set(record.log.id, {
6499
+ name: record.name,
6500
+ type: record.type ?? null,
6501
+ parent: record.parentId && namesById.get(record.parentId) || null
6502
+ });
6493
6503
  }
6494
6504
  if (!record.name) continue;
6495
6505
  const status2 = {
@@ -6509,7 +6519,7 @@ async function getBuildTimeline(context, cred, buildId) {
6509
6519
  if (b.startTime === void 0) return -1;
6510
6520
  return a.startTime < b.startTime ? -1 : 1;
6511
6521
  });
6512
- return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
6522
+ return { errors, stages, jobs: jobs.map((j) => j.status), logSteps, logRecords };
6513
6523
  }
6514
6524
  async function listTestRuns(context, cred, buildId) {
6515
6525
  const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
@@ -6618,16 +6628,18 @@ async function getRunLogs(context, cred, buildId) {
6618
6628
  );
6619
6629
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6620
6630
  const data = await readJsonResponse2(response);
6621
- let logSteps = /* @__PURE__ */ new Map();
6631
+ let logRecords = /* @__PURE__ */ new Map();
6622
6632
  try {
6623
- logSteps = (await getBuildTimeline(context, cred, buildId)).logSteps;
6633
+ logRecords = (await getBuildTimeline(context, cred, buildId)).logRecords;
6624
6634
  } catch {
6625
6635
  }
6626
6636
  return data.value.map((log) => ({
6627
6637
  id: log.id,
6628
6638
  createdOn: log.createdOn ?? null,
6629
6639
  lineCount: log.lineCount ?? null,
6630
- step: logSteps.get(log.id) ?? null
6640
+ step: logRecords.get(log.id)?.name ?? null,
6641
+ type: logRecords.get(log.id)?.type ?? null,
6642
+ parent: logRecords.get(log.id)?.parent ?? null
6631
6643
  }));
6632
6644
  }
6633
6645
  async function getRunLog(context, cred, buildId, logId) {
@@ -6640,6 +6652,122 @@ async function getRunLog(context, cred, buildId, logId) {
6640
6652
  }
6641
6653
  return response.text();
6642
6654
  }
6655
+ async function listBuildArtifacts(context, cred, buildId) {
6656
+ const url = withApiVersion(
6657
+ new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/artifacts`)
6658
+ );
6659
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6660
+ const data = await readJsonResponse2(response);
6661
+ return data.value.map((artifact) => {
6662
+ const size = Number.parseInt(artifact.resource?.properties?.artifactsize ?? "", 10);
6663
+ return {
6664
+ id: artifact.id,
6665
+ name: artifact.name,
6666
+ type: artifact.resource?.type ?? null,
6667
+ sizeBytes: Number.isFinite(size) ? size : null,
6668
+ downloadUrl: artifact.resource?.downloadUrl ?? null
6669
+ };
6670
+ });
6671
+ }
6672
+ var INITIAL_BUFFER_BYTES = 8 * 1024 * 1024;
6673
+ async function collectBody(body, declared, onProgress, initialBytes = INITIAL_BUFFER_BYTES) {
6674
+ const hint = Number.isSafeInteger(declared) && declared > 0 ? declared : Infinity;
6675
+ let buffer = new Uint8Array(Math.min(hint, initialBytes));
6676
+ let received = 0;
6677
+ for await (const chunk of body) {
6678
+ const needed = received + chunk.length;
6679
+ if (needed > buffer.length) {
6680
+ let capacity = Math.max(buffer.length * 2, needed);
6681
+ if (hint >= needed) capacity = Math.min(capacity, hint);
6682
+ const grown = new Uint8Array(capacity);
6683
+ grown.set(buffer.subarray(0, received));
6684
+ buffer = grown;
6685
+ }
6686
+ buffer.set(chunk, received);
6687
+ received = needed;
6688
+ onProgress?.(received);
6689
+ }
6690
+ return buffer.subarray(0, received);
6691
+ }
6692
+ async function downloadArtifactZip(cred, artifact, onProgress) {
6693
+ if (!artifact.downloadUrl) {
6694
+ throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
6695
+ }
6696
+ const url = new URL(artifact.downloadUrl);
6697
+ url.searchParams.set("$format", "zip");
6698
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6699
+ if (!response.ok) {
6700
+ throw httpError(response);
6701
+ }
6702
+ if (!response.body) {
6703
+ return new Uint8Array(await response.arrayBuffer());
6704
+ }
6705
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
6706
+ return collectBody(response.body, declared, onProgress);
6707
+ }
6708
+
6709
+ // src/services/artifact-extract.ts
6710
+ import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
6711
+ import path2 from "path";
6712
+ import { unzipSync } from "fflate";
6713
+ function entryTarget(entry, artifactName) {
6714
+ const normalized = entry.replaceAll("\\", "/");
6715
+ if (normalized.endsWith("/")) return null;
6716
+ const prefix = `${artifactName}/`;
6717
+ const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
6718
+ const segments = relative.split("/");
6719
+ if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
6720
+ throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
6721
+ }
6722
+ return relative;
6723
+ }
6724
+ function assertNoSymlinkEscape(root, relative) {
6725
+ let current = root;
6726
+ for (const segment of relative.split("/")) {
6727
+ current = path2.join(current, segment);
6728
+ let isLink;
6729
+ try {
6730
+ isLink = lstatSync(current).isSymbolicLink();
6731
+ } catch {
6732
+ return;
6733
+ }
6734
+ if (isLink) {
6735
+ throw new Error(`Refusing to extract through symbolic link "${current}".`);
6736
+ }
6737
+ }
6738
+ }
6739
+ function extractArtifactZip(zip, destination, options) {
6740
+ const entries = unzipSync(zip);
6741
+ const targets = [];
6742
+ const seen = /* @__PURE__ */ new Set();
6743
+ for (const [entry, data] of Object.entries(entries)) {
6744
+ const relative = entryTarget(entry, options.artifactName);
6745
+ if (relative === null) continue;
6746
+ const key = path2.posix.normalize(relative);
6747
+ if (seen.has(key)) {
6748
+ throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
6749
+ }
6750
+ seen.add(key);
6751
+ targets.push({ relative: key, data });
6752
+ }
6753
+ if (!options.force) {
6754
+ const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
6755
+ if (existing) {
6756
+ throw new Error(
6757
+ `"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
6758
+ );
6759
+ }
6760
+ }
6761
+ mkdirSync(destination, { recursive: true });
6762
+ const root = realpathSync(destination);
6763
+ for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
6764
+ for (const { relative, data } of targets) {
6765
+ const file = path2.join(root, relative);
6766
+ mkdirSync(path2.dirname(file), { recursive: true });
6767
+ writeFileSync(file, data);
6768
+ }
6769
+ return targets.map((t) => t.relative);
6770
+ }
6643
6771
 
6644
6772
  // src/commands/pipeline.ts
6645
6773
  var EXIT_FAILED = 1;
@@ -6976,19 +7104,26 @@ function formatRunDetail(detail) {
6976
7104
  ...testRows(detail)
6977
7105
  ].join("\n");
6978
7106
  }
7107
+ async function withRunContext(runIdRaw, options, body) {
7108
+ validateOrgProjectPair(options);
7109
+ const runId = parsePositiveId(runIdRaw);
7110
+ if (runId === null) {
7111
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7112
+ return;
7113
+ }
7114
+ let context;
7115
+ try {
7116
+ const resolved = await resolvePipelineContext(options);
7117
+ context = resolved.context;
7118
+ await body(runId, resolved);
7119
+ } catch (err) {
7120
+ handlePipelineError(err, context);
7121
+ }
7122
+ }
6979
7123
  function createPipelineGetRunDetailCommand() {
6980
7124
  const command = new Command14("get-run-detail");
6981
- 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) => {
6982
- validateOrgProjectPair(options);
6983
- const runId = parsePositiveId(runIdRaw);
6984
- if (runId === null) {
6985
- writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
6986
- return;
6987
- }
6988
- let context;
6989
- try {
6990
- const resolved = await resolvePipelineContext(options);
6991
- context = resolved.context;
7125
+ 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(
7126
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
6992
7127
  const detail = await getRunDetail(resolved.context, resolved.cred, runId);
6993
7128
  if (options.json) {
6994
7129
  process.stdout.write(`${JSON.stringify(detail, null, 2)}
@@ -6997,10 +7132,8 @@ function createPipelineGetRunDetailCommand() {
6997
7132
  }
6998
7133
  process.stdout.write(`${formatRunDetail(detail)}
6999
7134
  `);
7000
- } catch (err) {
7001
- handlePipelineError(err, context);
7002
- }
7003
- });
7135
+ })
7136
+ );
7004
7137
  return command;
7005
7138
  }
7006
7139
  function grepWithContext(lines, grep, context) {
@@ -7023,17 +7156,37 @@ function grepWithContext(lines, grep, context) {
7023
7156
  }
7024
7157
  return selected;
7025
7158
  }
7026
- function filterLogLines(content, grep, tail, context) {
7159
+ var PROGRESS_BAR = /(?<![\d.])\d+(?:\.\d+)? [KMGT]?i?B \/ \d+(?:\.\d+)? [KMGT]?i?B \[/g;
7160
+ var LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z /;
7161
+ function collapseGluedBars(line) {
7162
+ const starts = [...line.matchAll(PROGRESS_BAR)].map((m) => m.index ?? 0);
7163
+ if (starts.length < 2) return line;
7164
+ const prefix = LOG_TIMESTAMP.exec(line)?.[0] ?? "";
7165
+ return prefix + line.slice(starts.at(-1));
7166
+ }
7167
+ function collapseProgress(line) {
7168
+ let end = line.length;
7169
+ while (end > 0 && line[end - 1] === "\r") end--;
7170
+ return collapseGluedBars(line.slice(line.lastIndexOf("\r", end - 1) + 1, end));
7171
+ }
7172
+ function filterLogLines(content, filters) {
7173
+ const { grep, tail, head, contextLines: context } = filters;
7027
7174
  let lines = content.split("\n");
7028
7175
  if (lines.at(-1) === "") {
7029
7176
  lines.pop();
7030
7177
  }
7178
+ if (filters.noProgress) {
7179
+ lines = lines.map(collapseProgress);
7180
+ }
7031
7181
  if (grep) {
7032
7182
  lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
7033
7183
  }
7034
7184
  if (tail !== void 0 && lines.length > tail) {
7035
7185
  lines = lines.slice(-tail);
7036
7186
  }
7187
+ if (head !== void 0 && lines.length > head) {
7188
+ lines = lines.slice(0, head);
7189
+ }
7037
7190
  return lines;
7038
7191
  }
7039
7192
  function parseLogFilters(options) {
@@ -7042,9 +7195,14 @@ function parseLogFilters(options) {
7042
7195
  return null;
7043
7196
  }
7044
7197
  const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
7045
- const slices = options.tail !== void 0 || options.grep !== void 0 || options.context !== void 0;
7198
+ const noProgress = options.progress === false;
7199
+ const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
7046
7200
  if (slices && !selectsSingleLog) {
7047
- writeError2("--tail, --grep, and --context require --log-id or --step.");
7201
+ writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
7202
+ return null;
7203
+ }
7204
+ if (options.head !== void 0 && options.tail !== void 0) {
7205
+ writeError2("Use either --head or --tail, not both.");
7048
7206
  return null;
7049
7207
  }
7050
7208
  if (options.context !== void 0 && options.grep === void 0) {
@@ -7053,6 +7211,8 @@ function parseLogFilters(options) {
7053
7211
  }
7054
7212
  const tail = parseOptionalCount(options.tail, "--tail");
7055
7213
  if (tail === null) return null;
7214
+ const head = parseOptionalCount(options.head, "--head");
7215
+ if (head === null) return null;
7056
7216
  const contextLines = parseOptionalCount(options.context, "--context");
7057
7217
  if (contextLines === null) return null;
7058
7218
  let grep;
@@ -7064,7 +7224,13 @@ function parseLogFilters(options) {
7064
7224
  return null;
7065
7225
  }
7066
7226
  }
7067
- return { tail, contextLines: contextLines ?? 0, grep };
7227
+ return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
7228
+ }
7229
+ function describeLog(log) {
7230
+ const kind = log.type ?? "";
7231
+ const where = log.parent ? ` in ${log.parent}` : "";
7232
+ const detail = `${kind}${where}`.trim();
7233
+ return detail ? `${log.step} [${detail}]` : String(log.step);
7068
7234
  }
7069
7235
  function chooseStepLog(logs, step, runId) {
7070
7236
  const needle = step.toLowerCase();
@@ -7076,7 +7242,7 @@ function chooseStepLog(logs, step, runId) {
7076
7242
  return null;
7077
7243
  }
7078
7244
  if (chosen.length > 1) {
7079
- const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
7245
+ const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
7080
7246
  writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
7081
7247
  return null;
7082
7248
  }
@@ -7098,8 +7264,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
7098
7264
  return chooseStepLog(allLogs, options.step, runId);
7099
7265
  }
7100
7266
  function printSingleLog(content, filters) {
7101
- if (filters.grep !== void 0 || filters.tail !== void 0) {
7102
- const lines = filterLogLines(content, filters.grep, filters.tail, filters.contextLines);
7267
+ if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
7268
+ const lines = filterLogLines(content, filters);
7103
7269
  if (lines.length > 0) {
7104
7270
  process.stdout.write(`${lines.join("\n")}
7105
7271
  `);
@@ -7111,7 +7277,7 @@ function printSingleLog(content, filters) {
7111
7277
  }
7112
7278
  function createPipelineLogsCommand() {
7113
7279
  const command = new Command14("logs");
7114
- 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) => {
7280
+ command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--head <n>", "with --log-id/--step, print only the first N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--no-progress", "with --log-id/--step, collapse progress-bar redraws (carriage-return or glued into one line) to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
7115
7281
  validateOrgProjectPair(options);
7116
7282
  const runId = parsePositiveId(runIdRaw);
7117
7283
  if (runId === null) {
@@ -7150,7 +7316,9 @@ function createPipelineLogsCommand() {
7150
7316
  String(l.id),
7151
7317
  l.createdOn ?? "\u2014",
7152
7318
  l.lineCount == null ? "" : `${l.lineCount} lines`,
7153
- l.step ?? ""
7319
+ l.type ?? "",
7320
+ l.step ?? "",
7321
+ l.parent ? `(in ${l.parent})` : ""
7154
7322
  ]);
7155
7323
  process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
7156
7324
  `);
@@ -7160,6 +7328,102 @@ function createPipelineLogsCommand() {
7160
7328
  });
7161
7329
  return command;
7162
7330
  }
7331
+ function formatSize(bytes) {
7332
+ if (bytes === null) return "";
7333
+ if (bytes < 1024) return `${bytes} B`;
7334
+ const units = ["KB", "MB", "GB", "TB"];
7335
+ let value = bytes / 1024;
7336
+ let unit = 0;
7337
+ while (value >= 1024 && unit < units.length - 1) {
7338
+ value /= 1024;
7339
+ unit++;
7340
+ }
7341
+ return `${value.toFixed(1)} ${units[unit]}`;
7342
+ }
7343
+ function createPipelineArtifactsCommand() {
7344
+ const command = new Command14("artifacts");
7345
+ command.description("List the build artifacts published by a pipeline run").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
7346
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
7347
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7348
+ if (options.json) {
7349
+ process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
7350
+ `);
7351
+ return;
7352
+ }
7353
+ if (artifacts.length === 0) {
7354
+ process.stdout.write(`No artifacts found for run ${runId}.
7355
+ `);
7356
+ return;
7357
+ }
7358
+ const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
7359
+ process.stdout.write(`${formatTable(rows)}
7360
+ `);
7361
+ })
7362
+ );
7363
+ return command;
7364
+ }
7365
+ function selectArtifacts(artifacts, name, all, runId) {
7366
+ const available = artifacts.map((a) => a.name).join(", ") || "(none)";
7367
+ if (all) return artifacts;
7368
+ if (name === void 0) {
7369
+ writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
7370
+ return null;
7371
+ }
7372
+ const match = artifacts.find((a) => a.name === name);
7373
+ if (!match) {
7374
+ writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
7375
+ return null;
7376
+ }
7377
+ return [match];
7378
+ }
7379
+ function createPipelineArtifactDownloadCommand() {
7380
+ const command = new Command14("artifact-download");
7381
+ command.description("Download a run's artifact (or --all) and extract it into a folder; no zip is left behind").argument("<run_id>", "pipeline run id").argument("[name]", "artifact name (see `pipeline artifacts`)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--path <dir>", "destination folder (default ./<name>; with --all, <dir>/<name> per artifact)").option("--all", "download every artifact of the run").option("--force", "overwrite files that already exist").option("--progress", "print byte-progress lines on stderr").option("--json", "output JSON").action(async (runIdRaw, name, options) => {
7382
+ validateOrgProjectPair(options);
7383
+ const runId = parsePositiveId(runIdRaw);
7384
+ if (runId === null) {
7385
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7386
+ return;
7387
+ }
7388
+ if (options.all && name !== void 0) {
7389
+ writeError2("Use either an artifact name or --all, not both.");
7390
+ return;
7391
+ }
7392
+ let context;
7393
+ try {
7394
+ const resolved = await resolvePipelineContext(options);
7395
+ context = resolved.context;
7396
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7397
+ const selected = selectArtifacts(artifacts, name, options.all === true, runId);
7398
+ if (selected === null) return;
7399
+ const results = await Promise.all(
7400
+ selected.map(async (artifact) => {
7401
+ const destination = path3.resolve(
7402
+ options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
7403
+ );
7404
+ const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
7405
+ `) : void 0;
7406
+ const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
7407
+ const files = extractArtifactZip(zip, destination, {
7408
+ artifactName: artifact.name,
7409
+ force: options.force === true
7410
+ });
7411
+ return { name: artifact.name, path: destination, files: files.length };
7412
+ })
7413
+ );
7414
+ if (options.json) {
7415
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
7416
+ `);
7417
+ return;
7418
+ }
7419
+ process.stdout.write(`${results.map((r) => r.path).join("\n")}
7420
+ `);
7421
+ } catch (err) {
7422
+ handlePipelineError(err, context);
7423
+ }
7424
+ });
7425
+ return command;
7426
+ }
7163
7427
  function createPipelineTestsCommand() {
7164
7428
  const command = new Command14("tests");
7165
7429
  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) => {
@@ -7261,6 +7525,8 @@ function createPipelineCommand() {
7261
7525
  command.addCommand(createPipelineWaitCommand());
7262
7526
  command.addCommand(createPipelineGetRunDetailCommand());
7263
7527
  command.addCommand(createPipelineLogsCommand());
7528
+ command.addCommand(createPipelineArtifactsCommand());
7529
+ command.addCommand(createPipelineArtifactDownloadCommand());
7264
7530
  command.addCommand(createPipelineTestsCommand());
7265
7531
  command.addCommand(createPipelineStartCommand());
7266
7532
  return command;
@@ -7352,7 +7618,7 @@ function createCommentsCommand() {
7352
7618
  // src/commands/download-attachment.ts
7353
7619
  import { Command as Command16 } from "commander";
7354
7620
  import { writeFile as writeFile2 } from "fs/promises";
7355
- import { existsSync as existsSync6 } from "fs";
7621
+ import { existsSync as existsSync7 } from "fs";
7356
7622
  import { join as join3 } from "path";
7357
7623
  function createDownloadAttachmentCommand() {
7358
7624
  const command = new Command16("download-attachment");
@@ -7365,7 +7631,7 @@ function createDownloadAttachmentCommand() {
7365
7631
  context = resolveContext(options);
7366
7632
  const credential = await requireAuthCredential(context.org);
7367
7633
  const outputDir = options.output ?? ".";
7368
- if (!existsSync6(outputDir)) {
7634
+ if (!existsSync7(outputDir)) {
7369
7635
  process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
7370
7636
  `);
7371
7637
  process.exit(1);
@@ -7398,7 +7664,7 @@ function createDownloadAttachmentCommand() {
7398
7664
 
7399
7665
  // src/commands/add-attachment.ts
7400
7666
  import { Command as Command17 } from "commander";
7401
- import { existsSync as existsSync7, statSync } from "fs";
7667
+ import { existsSync as existsSync8, statSync } from "fs";
7402
7668
  import { readFile } from "fs/promises";
7403
7669
  import { basename } from "path";
7404
7670
  function createAddAttachmentCommand() {
@@ -7407,7 +7673,7 @@ function createAddAttachmentCommand() {
7407
7673
  async (idStr, file, options) => {
7408
7674
  const id = parseWorkItemId(idStr);
7409
7675
  validateOrgProjectPair(options);
7410
- if (!existsSync7(file)) {
7676
+ if (!existsSync8(file)) {
7411
7677
  process.stderr.write(`Error: File not found: ${file}
7412
7678
  `);
7413
7679
  process.exit(1);
@@ -7827,7 +8093,7 @@ function createRelationsCommand() {
7827
8093
 
7828
8094
  // src/services/update-check.ts
7829
8095
  import fs2 from "fs";
7830
- import path2 from "path";
8096
+ import path4 from "path";
7831
8097
  import os from "os";
7832
8098
  var THROTTLE_MS = 10 * 60 * 1e3;
7833
8099
  var FETCH_TIMEOUT_MS = 1500;
@@ -7845,7 +8111,7 @@ function skipsUpdateCheck(commandPath) {
7845
8111
  return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
7846
8112
  }
7847
8113
  function getCachePath() {
7848
- return path2.join(os.homedir(), ".azdo", "update-check.json");
8114
+ return path4.join(os.homedir(), ".azdo", "update-check.json");
7849
8115
  }
7850
8116
  function defaultReadCache() {
7851
8117
  try {
@@ -7857,7 +8123,7 @@ function defaultReadCache() {
7857
8123
  function defaultWriteCache(data) {
7858
8124
  try {
7859
8125
  const cachePath = getCachePath();
7860
- fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
8126
+ fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
7861
8127
  fs2.writeFileSync(cachePath, data);
7862
8128
  } catch {
7863
8129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.22.0-develop.798",
3
+ "version": "0.22.0-develop.822",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "@napi-rs/keyring": "^1.3.0",
29
29
  "@primno/dpapi": "^2.0.1",
30
30
  "commander": "^14.0.3",
31
+ "fflate": "^0.8.3",
31
32
  "jimp": "^1.6.1",
32
33
  "node-html-markdown": "^2.0.0"
33
34
  },