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

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 +289 -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,108 @@ 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
+ async function downloadArtifactZip(cred, artifact, onProgress) {
6673
+ if (!artifact.downloadUrl) {
6674
+ throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
6675
+ }
6676
+ const url = new URL(artifact.downloadUrl);
6677
+ url.searchParams.set("$format", "zip");
6678
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
6679
+ if (!response.ok) {
6680
+ throw httpError(response);
6681
+ }
6682
+ if (!response.body) {
6683
+ return new Uint8Array(await response.arrayBuffer());
6684
+ }
6685
+ const chunks = [];
6686
+ let received = 0;
6687
+ for await (const chunk of response.body) {
6688
+ chunks.push(chunk);
6689
+ received += chunk.length;
6690
+ onProgress?.(received);
6691
+ }
6692
+ return Buffer.concat(chunks);
6693
+ }
6694
+
6695
+ // src/services/artifact-extract.ts
6696
+ import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
6697
+ import path2 from "path";
6698
+ import { unzipSync } from "fflate";
6699
+ function entryTarget(entry, artifactName) {
6700
+ const normalized = entry.replaceAll("\\", "/");
6701
+ if (normalized.endsWith("/")) return null;
6702
+ const prefix = `${artifactName}/`;
6703
+ const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
6704
+ const segments = relative.split("/");
6705
+ if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
6706
+ throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
6707
+ }
6708
+ return relative;
6709
+ }
6710
+ function assertNoSymlinkEscape(root, relative) {
6711
+ let current = root;
6712
+ for (const segment of relative.split("/")) {
6713
+ current = path2.join(current, segment);
6714
+ let isLink;
6715
+ try {
6716
+ isLink = lstatSync(current).isSymbolicLink();
6717
+ } catch {
6718
+ return;
6719
+ }
6720
+ if (isLink) {
6721
+ throw new Error(`Refusing to extract through symbolic link "${current}".`);
6722
+ }
6723
+ }
6724
+ }
6725
+ function extractArtifactZip(zip, destination, options) {
6726
+ const entries = unzipSync(zip);
6727
+ const targets = [];
6728
+ const seen = /* @__PURE__ */ new Set();
6729
+ for (const [entry, data] of Object.entries(entries)) {
6730
+ const relative = entryTarget(entry, options.artifactName);
6731
+ if (relative === null) continue;
6732
+ const key = path2.posix.normalize(relative);
6733
+ if (seen.has(key)) {
6734
+ throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
6735
+ }
6736
+ seen.add(key);
6737
+ targets.push({ relative: key, data });
6738
+ }
6739
+ if (!options.force) {
6740
+ const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
6741
+ if (existing) {
6742
+ throw new Error(
6743
+ `"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
6744
+ );
6745
+ }
6746
+ }
6747
+ mkdirSync(destination, { recursive: true });
6748
+ const root = realpathSync(destination);
6749
+ for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
6750
+ for (const { relative, data } of targets) {
6751
+ const file = path2.join(root, relative);
6752
+ mkdirSync(path2.dirname(file), { recursive: true });
6753
+ writeFileSync(file, data);
6754
+ }
6755
+ return targets.map((t) => t.relative);
6756
+ }
6643
6757
 
6644
6758
  // src/commands/pipeline.ts
6645
6759
  var EXIT_FAILED = 1;
@@ -6976,19 +7090,26 @@ function formatRunDetail(detail) {
6976
7090
  ...testRows(detail)
6977
7091
  ].join("\n");
6978
7092
  }
7093
+ async function withRunContext(runIdRaw, options, body) {
7094
+ validateOrgProjectPair(options);
7095
+ const runId = parsePositiveId(runIdRaw);
7096
+ if (runId === null) {
7097
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7098
+ return;
7099
+ }
7100
+ let context;
7101
+ try {
7102
+ const resolved = await resolvePipelineContext(options);
7103
+ context = resolved.context;
7104
+ await body(runId, resolved);
7105
+ } catch (err) {
7106
+ handlePipelineError(err, context);
7107
+ }
7108
+ }
6979
7109
  function createPipelineGetRunDetailCommand() {
6980
7110
  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;
7111
+ 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(
7112
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
6992
7113
  const detail = await getRunDetail(resolved.context, resolved.cred, runId);
6993
7114
  if (options.json) {
6994
7115
  process.stdout.write(`${JSON.stringify(detail, null, 2)}
@@ -6997,10 +7118,8 @@ function createPipelineGetRunDetailCommand() {
6997
7118
  }
6998
7119
  process.stdout.write(`${formatRunDetail(detail)}
6999
7120
  `);
7000
- } catch (err) {
7001
- handlePipelineError(err, context);
7002
- }
7003
- });
7121
+ })
7122
+ );
7004
7123
  return command;
7005
7124
  }
7006
7125
  function grepWithContext(lines, grep, context) {
@@ -7023,17 +7142,29 @@ function grepWithContext(lines, grep, context) {
7023
7142
  }
7024
7143
  return selected;
7025
7144
  }
7026
- function filterLogLines(content, grep, tail, context) {
7145
+ function collapseProgress(line) {
7146
+ let end = line.length;
7147
+ while (end > 0 && line[end - 1] === "\r") end--;
7148
+ return line.slice(line.lastIndexOf("\r", end - 1) + 1, end);
7149
+ }
7150
+ function filterLogLines(content, filters) {
7151
+ const { grep, tail, head, contextLines: context } = filters;
7027
7152
  let lines = content.split("\n");
7028
7153
  if (lines.at(-1) === "") {
7029
7154
  lines.pop();
7030
7155
  }
7156
+ if (filters.noProgress) {
7157
+ lines = lines.map(collapseProgress);
7158
+ }
7031
7159
  if (grep) {
7032
7160
  lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
7033
7161
  }
7034
7162
  if (tail !== void 0 && lines.length > tail) {
7035
7163
  lines = lines.slice(-tail);
7036
7164
  }
7165
+ if (head !== void 0 && lines.length > head) {
7166
+ lines = lines.slice(0, head);
7167
+ }
7037
7168
  return lines;
7038
7169
  }
7039
7170
  function parseLogFilters(options) {
@@ -7042,9 +7173,14 @@ function parseLogFilters(options) {
7042
7173
  return null;
7043
7174
  }
7044
7175
  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;
7176
+ const noProgress = options.progress === false;
7177
+ const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
7046
7178
  if (slices && !selectsSingleLog) {
7047
- writeError2("--tail, --grep, and --context require --log-id or --step.");
7179
+ writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
7180
+ return null;
7181
+ }
7182
+ if (options.head !== void 0 && options.tail !== void 0) {
7183
+ writeError2("Use either --head or --tail, not both.");
7048
7184
  return null;
7049
7185
  }
7050
7186
  if (options.context !== void 0 && options.grep === void 0) {
@@ -7053,6 +7189,8 @@ function parseLogFilters(options) {
7053
7189
  }
7054
7190
  const tail = parseOptionalCount(options.tail, "--tail");
7055
7191
  if (tail === null) return null;
7192
+ const head = parseOptionalCount(options.head, "--head");
7193
+ if (head === null) return null;
7056
7194
  const contextLines = parseOptionalCount(options.context, "--context");
7057
7195
  if (contextLines === null) return null;
7058
7196
  let grep;
@@ -7064,7 +7202,13 @@ function parseLogFilters(options) {
7064
7202
  return null;
7065
7203
  }
7066
7204
  }
7067
- return { tail, contextLines: contextLines ?? 0, grep };
7205
+ return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
7206
+ }
7207
+ function describeLog(log) {
7208
+ const kind = log.type ?? "";
7209
+ const where = log.parent ? ` in ${log.parent}` : "";
7210
+ const detail = `${kind}${where}`.trim();
7211
+ return detail ? `${log.step} [${detail}]` : String(log.step);
7068
7212
  }
7069
7213
  function chooseStepLog(logs, step, runId) {
7070
7214
  const needle = step.toLowerCase();
@@ -7076,7 +7220,7 @@ function chooseStepLog(logs, step, runId) {
7076
7220
  return null;
7077
7221
  }
7078
7222
  if (chosen.length > 1) {
7079
- const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
7223
+ const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
7080
7224
  writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
7081
7225
  return null;
7082
7226
  }
@@ -7098,8 +7242,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
7098
7242
  return chooseStepLog(allLogs, options.step, runId);
7099
7243
  }
7100
7244
  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);
7245
+ if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
7246
+ const lines = filterLogLines(content, filters);
7103
7247
  if (lines.length > 0) {
7104
7248
  process.stdout.write(`${lines.join("\n")}
7105
7249
  `);
@@ -7111,7 +7255,7 @@ function printSingleLog(content, filters) {
7111
7255
  }
7112
7256
  function createPipelineLogsCommand() {
7113
7257
  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) => {
7258
+ 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 carriage-return progress redraws to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
7115
7259
  validateOrgProjectPair(options);
7116
7260
  const runId = parsePositiveId(runIdRaw);
7117
7261
  if (runId === null) {
@@ -7150,7 +7294,9 @@ function createPipelineLogsCommand() {
7150
7294
  String(l.id),
7151
7295
  l.createdOn ?? "\u2014",
7152
7296
  l.lineCount == null ? "" : `${l.lineCount} lines`,
7153
- l.step ?? ""
7297
+ l.type ?? "",
7298
+ l.step ?? "",
7299
+ l.parent ? `(in ${l.parent})` : ""
7154
7300
  ]);
7155
7301
  process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
7156
7302
  `);
@@ -7160,6 +7306,102 @@ function createPipelineLogsCommand() {
7160
7306
  });
7161
7307
  return command;
7162
7308
  }
7309
+ function formatSize(bytes) {
7310
+ if (bytes === null) return "";
7311
+ if (bytes < 1024) return `${bytes} B`;
7312
+ const units = ["KB", "MB", "GB", "TB"];
7313
+ let value = bytes / 1024;
7314
+ let unit = 0;
7315
+ while (value >= 1024 && unit < units.length - 1) {
7316
+ value /= 1024;
7317
+ unit++;
7318
+ }
7319
+ return `${value.toFixed(1)} ${units[unit]}`;
7320
+ }
7321
+ function createPipelineArtifactsCommand() {
7322
+ const command = new Command14("artifacts");
7323
+ 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(
7324
+ (runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
7325
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7326
+ if (options.json) {
7327
+ process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
7328
+ `);
7329
+ return;
7330
+ }
7331
+ if (artifacts.length === 0) {
7332
+ process.stdout.write(`No artifacts found for run ${runId}.
7333
+ `);
7334
+ return;
7335
+ }
7336
+ const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
7337
+ process.stdout.write(`${formatTable(rows)}
7338
+ `);
7339
+ })
7340
+ );
7341
+ return command;
7342
+ }
7343
+ function selectArtifacts(artifacts, name, all, runId) {
7344
+ const available = artifacts.map((a) => a.name).join(", ") || "(none)";
7345
+ if (all) return artifacts;
7346
+ if (name === void 0) {
7347
+ writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
7348
+ return null;
7349
+ }
7350
+ const match = artifacts.find((a) => a.name === name);
7351
+ if (!match) {
7352
+ writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
7353
+ return null;
7354
+ }
7355
+ return [match];
7356
+ }
7357
+ function createPipelineArtifactDownloadCommand() {
7358
+ const command = new Command14("artifact-download");
7359
+ 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) => {
7360
+ validateOrgProjectPair(options);
7361
+ const runId = parsePositiveId(runIdRaw);
7362
+ if (runId === null) {
7363
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
7364
+ return;
7365
+ }
7366
+ if (options.all && name !== void 0) {
7367
+ writeError2("Use either an artifact name or --all, not both.");
7368
+ return;
7369
+ }
7370
+ let context;
7371
+ try {
7372
+ const resolved = await resolvePipelineContext(options);
7373
+ context = resolved.context;
7374
+ const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
7375
+ const selected = selectArtifacts(artifacts, name, options.all === true, runId);
7376
+ if (selected === null) return;
7377
+ const results = await Promise.all(
7378
+ selected.map(async (artifact) => {
7379
+ const destination = path3.resolve(
7380
+ options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
7381
+ );
7382
+ const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
7383
+ `) : void 0;
7384
+ const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
7385
+ const files = extractArtifactZip(zip, destination, {
7386
+ artifactName: artifact.name,
7387
+ force: options.force === true
7388
+ });
7389
+ return { name: artifact.name, path: destination, files: files.length };
7390
+ })
7391
+ );
7392
+ if (options.json) {
7393
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
7394
+ `);
7395
+ return;
7396
+ }
7397
+ process.stdout.write(`${results.map((r) => r.path).join("\n")}
7398
+ `);
7399
+ } catch (err) {
7400
+ handlePipelineError(err, context);
7401
+ }
7402
+ });
7403
+ return command;
7404
+ }
7163
7405
  function createPipelineTestsCommand() {
7164
7406
  const command = new Command14("tests");
7165
7407
  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 +7503,8 @@ function createPipelineCommand() {
7261
7503
  command.addCommand(createPipelineWaitCommand());
7262
7504
  command.addCommand(createPipelineGetRunDetailCommand());
7263
7505
  command.addCommand(createPipelineLogsCommand());
7506
+ command.addCommand(createPipelineArtifactsCommand());
7507
+ command.addCommand(createPipelineArtifactDownloadCommand());
7264
7508
  command.addCommand(createPipelineTestsCommand());
7265
7509
  command.addCommand(createPipelineStartCommand());
7266
7510
  return command;
@@ -7352,7 +7596,7 @@ function createCommentsCommand() {
7352
7596
  // src/commands/download-attachment.ts
7353
7597
  import { Command as Command16 } from "commander";
7354
7598
  import { writeFile as writeFile2 } from "fs/promises";
7355
- import { existsSync as existsSync6 } from "fs";
7599
+ import { existsSync as existsSync7 } from "fs";
7356
7600
  import { join as join3 } from "path";
7357
7601
  function createDownloadAttachmentCommand() {
7358
7602
  const command = new Command16("download-attachment");
@@ -7365,7 +7609,7 @@ function createDownloadAttachmentCommand() {
7365
7609
  context = resolveContext(options);
7366
7610
  const credential = await requireAuthCredential(context.org);
7367
7611
  const outputDir = options.output ?? ".";
7368
- if (!existsSync6(outputDir)) {
7612
+ if (!existsSync7(outputDir)) {
7369
7613
  process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
7370
7614
  `);
7371
7615
  process.exit(1);
@@ -7398,7 +7642,7 @@ function createDownloadAttachmentCommand() {
7398
7642
 
7399
7643
  // src/commands/add-attachment.ts
7400
7644
  import { Command as Command17 } from "commander";
7401
- import { existsSync as existsSync7, statSync } from "fs";
7645
+ import { existsSync as existsSync8, statSync } from "fs";
7402
7646
  import { readFile } from "fs/promises";
7403
7647
  import { basename } from "path";
7404
7648
  function createAddAttachmentCommand() {
@@ -7407,7 +7651,7 @@ function createAddAttachmentCommand() {
7407
7651
  async (idStr, file, options) => {
7408
7652
  const id = parseWorkItemId(idStr);
7409
7653
  validateOrgProjectPair(options);
7410
- if (!existsSync7(file)) {
7654
+ if (!existsSync8(file)) {
7411
7655
  process.stderr.write(`Error: File not found: ${file}
7412
7656
  `);
7413
7657
  process.exit(1);
@@ -7827,7 +8071,7 @@ function createRelationsCommand() {
7827
8071
 
7828
8072
  // src/services/update-check.ts
7829
8073
  import fs2 from "fs";
7830
- import path2 from "path";
8074
+ import path4 from "path";
7831
8075
  import os from "os";
7832
8076
  var THROTTLE_MS = 10 * 60 * 1e3;
7833
8077
  var FETCH_TIMEOUT_MS = 1500;
@@ -7845,7 +8089,7 @@ function skipsUpdateCheck(commandPath) {
7845
8089
  return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
7846
8090
  }
7847
8091
  function getCachePath() {
7848
- return path2.join(os.homedir(), ".azdo", "update-check.json");
8092
+ return path4.join(os.homedir(), ".azdo", "update-check.json");
7849
8093
  }
7850
8094
  function defaultReadCache() {
7851
8095
  try {
@@ -7857,7 +8101,7 @@ function defaultReadCache() {
7857
8101
  function defaultWriteCache(data) {
7858
8102
  try {
7859
8103
  const cachePath = getCachePath();
7860
- fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
8104
+ fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
7861
8105
  fs2.writeFileSync(cachePath, data);
7862
8106
  } catch {
7863
8107
  }
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.807",
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
  },