azdo-cli 0.10.0-develop.386 → 0.10.0-develop.423

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 +10 -1
  2. package/dist/index.js +1091 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,9 +56,18 @@ azdo comments add 12345 "Investigating the root cause now."
56
56
  # PR comment threads — list, filter, target by number, resolve or reopen
57
57
  azdo pr comments # active-branch PR
58
58
  azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
59
- azdo pr comments --pr-number 64 --hide-resolved
59
+ azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
60
+ azdo pr comments --code-related-only # only file/line-anchored threads
61
+ azdo pr status # PR checks (status + branch policies) + code-comment counts
60
62
  azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
61
63
  azdo pr comment-reopen 17 --pr-number 64
64
+
65
+ # Pipelines — list, inspect runs, wait (exit code = result), start
66
+ azdo pipeline list --filter ci
67
+ azdo pipeline get-runs 12 --branch develop --limit 1
68
+ azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
69
+ azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
70
+ azdo pipeline start 12 --branch develop --parameter env=staging
62
71
  ```
63
72
 
64
73
  ## Documentation
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  } from "./chunk-C7RAZJHV.js";
32
32
 
33
33
  // src/index.ts
34
- import { Command as Command15 } from "commander";
34
+ import { Command as Command16 } from "commander";
35
35
 
36
36
  // src/version.ts
37
37
  import { readFileSync } from "fs";
@@ -479,13 +479,13 @@ async function classifyDeviceTokenResponse(response) {
479
479
  async function pollForDeviceToken(deviceCode, oauthConfig, initialIntervalSec, expiresAtMs, deps) {
480
480
  const fetchFn = deps.fetch ?? fetch;
481
481
  const now = deps.now ?? (() => Date.now());
482
- const sleep = deps.sleep ?? defaultSleep;
482
+ const sleep2 = deps.sleep ?? defaultSleep;
483
483
  let intervalSec = Math.max(MIN_INTERVAL_SEC, initialIntervalSec);
484
484
  for (; ; ) {
485
485
  if (now() >= expiresAtMs) {
486
486
  throw new DeviceCodeFlowError("expired_token", "device-code flow expired before authorisation completed");
487
487
  }
488
- await sleep(intervalSec * 1e3);
488
+ await sleep2(intervalSec * 1e3);
489
489
  const body = new URLSearchParams({
490
490
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
491
491
  client_id: oauthConfig.clientId,
@@ -2652,6 +2652,21 @@ function buildPullRequestStatusesUrl(context, repo, prId) {
2652
2652
  url.searchParams.set("api-version", "7.1");
2653
2653
  return url;
2654
2654
  }
2655
+ function buildProjectUrl(context) {
2656
+ const url = new URL(
2657
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/projects/${encodeURIComponent(context.project)}`
2658
+ );
2659
+ url.searchParams.set("api-version", "7.1");
2660
+ return url;
2661
+ }
2662
+ function buildPolicyEvaluationsUrl(context, projectId, prId) {
2663
+ const url = new URL(
2664
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/policy/evaluations`
2665
+ );
2666
+ url.searchParams.set("api-version", "7.1");
2667
+ url.searchParams.set("artifactId", `vstfs:///CodeReview/CodeReviewId/${projectId}/${prId}`);
2668
+ return url;
2669
+ }
2655
2670
  function mapPullRequest(repo, pullRequest) {
2656
2671
  return {
2657
2672
  id: pullRequest.pullRequestId,
@@ -2690,7 +2705,49 @@ function mapPullRequestCheck(status2) {
2690
2705
  targetUrl: status2.targetUrl ?? null,
2691
2706
  createdBy: status2.createdBy?.displayName ?? null,
2692
2707
  createdAt: status2.creationDate ?? null,
2693
- updatedAt: status2.updatedDate ?? null
2708
+ updatedAt: status2.updatedDate ?? null,
2709
+ source: "status"
2710
+ };
2711
+ }
2712
+ function mapPolicyEvaluationState(status2) {
2713
+ switch (status2) {
2714
+ case "approved":
2715
+ return "succeeded";
2716
+ case "rejected":
2717
+ return "failed";
2718
+ case "running":
2719
+ case "queued":
2720
+ return "pending";
2721
+ case "notApplicable":
2722
+ case "notSet":
2723
+ case void 0:
2724
+ return null;
2725
+ default:
2726
+ return status2;
2727
+ }
2728
+ }
2729
+ function mapPolicyEvaluationName(evaluation) {
2730
+ const display = evaluation.configuration?.settings?.displayName?.trim() || evaluation.configuration?.type?.displayName?.trim();
2731
+ if (display) {
2732
+ return display;
2733
+ }
2734
+ return `Policy ${evaluation.configuration?.id ?? evaluation.evaluationId ?? "?"}`;
2735
+ }
2736
+ function mapPolicyEvaluationCheck(evaluation) {
2737
+ const state = mapPolicyEvaluationState(evaluation.status);
2738
+ if (state === null) {
2739
+ return null;
2740
+ }
2741
+ return {
2742
+ id: evaluation.configuration?.id ?? 0,
2743
+ state,
2744
+ name: mapPolicyEvaluationName(evaluation),
2745
+ description: null,
2746
+ targetUrl: null,
2747
+ createdBy: null,
2748
+ createdAt: null,
2749
+ updatedAt: null,
2750
+ source: "policy"
2694
2751
  };
2695
2752
  }
2696
2753
  function mapComment(comment) {
@@ -2776,6 +2833,21 @@ async function getPullRequestChecks(context, repo, cred, prId) {
2776
2833
  const data = await readJsonResponse(response);
2777
2834
  return data.value.map(mapPullRequestCheck).filter((check) => check !== null);
2778
2835
  }
2836
+ async function resolveProjectId(context, cred) {
2837
+ const response = await fetchWithErrors(buildProjectUrl(context).toString(), {
2838
+ headers: authHeaders(cred)
2839
+ });
2840
+ const data = await readJsonResponse(response);
2841
+ return data.id;
2842
+ }
2843
+ async function getPullRequestPolicyEvaluations(context, cred, projectId, prId) {
2844
+ const response = await fetchWithErrors(
2845
+ buildPolicyEvaluationsUrl(context, projectId, prId).toString(),
2846
+ { headers: authHeaders(cred) }
2847
+ );
2848
+ const data = await readJsonResponse(response);
2849
+ return data.value.map(mapPolicyEvaluationCheck).filter((check) => check !== null);
2850
+ }
2779
2851
  async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
2780
2852
  const existing = await listPullRequests(context, repo, cred, sourceBranch, {
2781
2853
  status: "active",
@@ -2884,7 +2956,10 @@ function handlePrCommandError(err, context, mode = "read") {
2884
2956
  }
2885
2957
  writeError(error.message);
2886
2958
  }
2887
- function formatPullRequestChecks(checks) {
2959
+ function formatPullRequestChecks(checks, checksError) {
2960
+ if (checksError) {
2961
+ return [`Checks: unable to retrieve (${checksError})`];
2962
+ }
2888
2963
  if (checks.length === 0) {
2889
2964
  return ["Checks: none reported by Azure DevOps"];
2890
2965
  }
@@ -2897,14 +2972,68 @@ function formatPullRequestChecks(checks) {
2897
2972
  }
2898
2973
  return lines;
2899
2974
  }
2975
+ function countCodeComments(threads) {
2976
+ let open = 0;
2977
+ let closed = 0;
2978
+ for (const thread of threads) {
2979
+ if (thread.threadContext === null) {
2980
+ continue;
2981
+ }
2982
+ if (isThreadResolved(thread.status)) {
2983
+ closed += 1;
2984
+ } else {
2985
+ open += 1;
2986
+ }
2987
+ }
2988
+ return { open, closed };
2989
+ }
2990
+ function formatCodeCommentCounts(counts) {
2991
+ return `Code comments: ${counts.open} open, ${counts.closed} closed`;
2992
+ }
2900
2993
  function formatPullRequestBlock(pullRequest) {
2901
2994
  return [
2902
2995
  `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
2903
2996
  `${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
2904
2997
  pullRequest.url ?? "\u2014",
2905
- ...formatPullRequestChecks(pullRequest.checks)
2998
+ ...formatPullRequestChecks(pullRequest.checks, pullRequest.checksError),
2999
+ formatCodeCommentCounts(pullRequest.codeCommentCounts)
2906
3000
  ].join("\n");
2907
3001
  }
3002
+ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, projectId) {
3003
+ let statusChecks = [];
3004
+ let statusOk = true;
3005
+ try {
3006
+ statusChecks = await getPullRequestChecks(context, repo, cred, pullRequest.id);
3007
+ } catch {
3008
+ statusOk = false;
3009
+ }
3010
+ let policyChecks = [];
3011
+ let policyOk = true;
3012
+ if (projectId === null) {
3013
+ policyOk = false;
3014
+ } else {
3015
+ try {
3016
+ policyChecks = await getPullRequestPolicyEvaluations(context, cred, projectId, pullRequest.id);
3017
+ } catch {
3018
+ policyOk = false;
3019
+ }
3020
+ }
3021
+ let codeCommentCounts;
3022
+ try {
3023
+ const threads = await getPullRequestThreads(context, repo, cred, pullRequest.id);
3024
+ codeCommentCounts = countCodeComments(threads);
3025
+ } catch {
3026
+ codeCommentCounts = { open: 0, closed: 0 };
3027
+ }
3028
+ const checks = [...statusChecks, ...policyChecks];
3029
+ const checksError = checks.length === 0 && (!statusOk || !policyOk) ? "Azure DevOps request failed" : null;
3030
+ return {
3031
+ ...pullRequest,
3032
+ checks,
3033
+ codeCommentCounts,
3034
+ checksError
3035
+ };
3036
+ }
2908
3037
  function threadStatusLabel(status2) {
2909
3038
  return isThreadResolved(status2) ? "resolved" : status2;
2910
3039
  }
@@ -2941,11 +3070,16 @@ function createPrStatusCommand() {
2941
3070
  context = resolved.context;
2942
3071
  const branch = resolved.branch;
2943
3072
  const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
3073
+ let projectId = null;
3074
+ try {
3075
+ projectId = await resolveProjectId(resolved.context, resolved.pat);
3076
+ } catch {
3077
+ projectId = null;
3078
+ }
2944
3079
  const pullRequestsWithChecks = await Promise.all(
2945
- pullRequests.map(async (pullRequest) => ({
2946
- ...pullRequest,
2947
- checks: await getPullRequestChecks(resolved.context, resolved.repo, resolved.pat, pullRequest.id)
2948
- }))
3080
+ pullRequests.map(
3081
+ async (pullRequest) => buildPullRequestStatusEntry(resolved.context, resolved.repo, resolved.pat, pullRequest, projectId)
3082
+ )
2949
3083
  );
2950
3084
  const result = { branch, repository: resolved.repo, pullRequests: pullRequestsWithChecks };
2951
3085
  if (options.json) {
@@ -3026,7 +3160,7 @@ ${result.pullRequest.url ?? "\u2014"}
3026
3160
  }
3027
3161
  function createPrCommentsCommand() {
3028
3162
  const command = new Command12("comments");
3029
- configureUnwrappedHelp(command).description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
3163
+ configureUnwrappedHelp(command).description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--exclude-resolved", "alias of --hide-resolved: exclude resolved / won't fix / closed / by design threads").option("--code-related-only", "show only threads anchored to a file/line; omit general discussion threads").option("--json", "output JSON").action(async (options) => {
3030
3164
  validateOrgProjectPair(options);
3031
3165
  let context;
3032
3166
  let explicitPrId = null;
@@ -3068,8 +3202,12 @@ function createPrCommentsCommand() {
3068
3202
  pullRequest = pullRequests[0];
3069
3203
  branchLabel = resolved.branch;
3070
3204
  }
3205
+ const hideResolved = options.hideResolved === true || options.excludeResolved === true;
3206
+ const codeRelatedOnly = options.codeRelatedOnly === true;
3071
3207
  const allThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
3072
- const threads = options.hideResolved ? allThreads.filter((thread) => !isThreadResolved(thread.status)) : allThreads;
3208
+ const threads = allThreads.filter(
3209
+ (thread) => (!hideResolved || !isThreadResolved(thread.status)) && (!codeRelatedOnly || thread.threadContext !== null)
3210
+ );
3073
3211
  const result = { branch: branchLabel, pullRequest, threads };
3074
3212
  if (options.json) {
3075
3213
  process.stdout.write(`${JSON.stringify(result, null, 2)}
@@ -3077,9 +3215,18 @@ function createPrCommentsCommand() {
3077
3215
  return;
3078
3216
  }
3079
3217
  if (threads.length === 0) {
3080
- if (options.hideResolved && allThreads.length > 0) {
3081
- process.stdout.write(`Pull request #${pullRequest.id} has no unresolved comment threads (${allThreads.length} resolved thread${allThreads.length === 1 ? "" : "s"} hidden by --hide-resolved).
3082
- `);
3218
+ if (allThreads.length > 0 && (hideResolved || codeRelatedOnly)) {
3219
+ const filters = [];
3220
+ if (codeRelatedOnly) {
3221
+ filters.push("code-related");
3222
+ }
3223
+ if (hideResolved) {
3224
+ filters.push("unresolved");
3225
+ }
3226
+ process.stdout.write(
3227
+ `Pull request #${pullRequest.id} has no ${filters.join(" ")} comment threads (filtered from ${allThreads.length} thread${allThreads.length === 1 ? "" : "s"}).
3228
+ `
3229
+ );
3083
3230
  } else {
3084
3231
  process.stdout.write(`Pull request #${pullRequest.id} has no comment threads.
3085
3232
  `);
@@ -3221,10 +3368,922 @@ function createPrCommand() {
3221
3368
  return command;
3222
3369
  }
3223
3370
 
3224
- // src/commands/comments.ts
3371
+ // src/commands/pipeline.ts
3225
3372
  import { Command as Command13 } from "commander";
3373
+
3374
+ // src/services/pipeline-client.ts
3375
+ var API_VERSION = "7.1";
3376
+ function orgProjectBase(context) {
3377
+ return `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}`;
3378
+ }
3379
+ function withApiVersion(url) {
3380
+ url.searchParams.set("api-version", API_VERSION);
3381
+ return url;
3382
+ }
3383
+ async function readJsonResponse2(response) {
3384
+ if (!response.ok) {
3385
+ throw new Error(`HTTP_${response.status}`);
3386
+ }
3387
+ return await response.json();
3388
+ }
3389
+ function mapPipeline(pipeline) {
3390
+ return {
3391
+ id: pipeline.id,
3392
+ name: pipeline.name,
3393
+ folder: pipeline.folder?.trim() ? pipeline.folder : null
3394
+ };
3395
+ }
3396
+ function mapRunState(state) {
3397
+ if (state === "inProgress" || state === "completed") {
3398
+ return state;
3399
+ }
3400
+ return "unknown";
3401
+ }
3402
+ function mapRunResult(result) {
3403
+ if (result === "succeeded" || result === "failed" || result === "canceled") {
3404
+ return result;
3405
+ }
3406
+ if (result === "partiallySucceeded") {
3407
+ return "failed";
3408
+ }
3409
+ return null;
3410
+ }
3411
+ function mapBuildState(status2) {
3412
+ if (status2 === "completed") return "completed";
3413
+ if (status2 === void 0 || status2 === "none") return "unknown";
3414
+ return "inProgress";
3415
+ }
3416
+ function mapBuildSummary(build) {
3417
+ return {
3418
+ id: build.id,
3419
+ name: build.buildNumber ?? null,
3420
+ state: mapBuildState(build.status),
3421
+ result: mapRunResult(build.result),
3422
+ createdDate: build.queueTime ?? build.startTime ?? null,
3423
+ finishedDate: build.finishTime ?? null,
3424
+ sourceBranch: build.sourceBranch ?? null,
3425
+ sourceCommit: build.sourceVersion ?? null
3426
+ };
3427
+ }
3428
+ function normalizeRef(branch) {
3429
+ return branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
3430
+ }
3431
+ async function getPipelineDefinitions(context, cred) {
3432
+ const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/pipelines`));
3433
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3434
+ const data = await readJsonResponse2(response);
3435
+ return data.value.map(mapPipeline);
3436
+ }
3437
+ var COMMIT_LOOKBACK = 200;
3438
+ async function getPipelineRuns(context, cred, query) {
3439
+ const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds`));
3440
+ if (query.definitionId !== void 0) {
3441
+ url.searchParams.set("definitions", String(query.definitionId));
3442
+ }
3443
+ if (query.prNumber !== void 0) {
3444
+ url.searchParams.set("branchName", `refs/pull/${query.prNumber}/merge`);
3445
+ } else if (query.branch) {
3446
+ url.searchParams.set("branchName", normalizeRef(query.branch));
3447
+ }
3448
+ url.searchParams.set("queryOrder", "queueTimeDescending");
3449
+ url.searchParams.set("$top", String(query.commit ? COMMIT_LOOKBACK : query.top));
3450
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3451
+ const data = await readJsonResponse2(response);
3452
+ let runs = data.value.map(mapBuildSummary);
3453
+ if (query.commit) {
3454
+ const needle = query.commit.toLowerCase();
3455
+ runs = runs.filter((run) => run.sourceCommit?.toLowerCase().startsWith(needle));
3456
+ }
3457
+ return runs.slice(0, query.top);
3458
+ }
3459
+ async function runPipeline(context, cred, pipelineId, opts) {
3460
+ const url = withApiVersion(
3461
+ new URL(`${orgProjectBase(context)}/_apis/pipelines/${pipelineId}/runs`)
3462
+ );
3463
+ const body = {};
3464
+ if (opts.branch) {
3465
+ const refName = opts.branch.startsWith("refs/") ? opts.branch : `refs/heads/${opts.branch}`;
3466
+ body.resources = { repositories: { self: { refName } } };
3467
+ }
3468
+ if (opts.parameters && Object.keys(opts.parameters).length > 0) {
3469
+ body.templateParameters = opts.parameters;
3470
+ }
3471
+ const response = await fetchWithErrors(url.toString(), {
3472
+ method: "POST",
3473
+ headers: { ...authHeaders(cred), "Content-Type": "application/json" },
3474
+ body: JSON.stringify(body)
3475
+ });
3476
+ const data = await readJsonResponse2(response);
3477
+ return {
3478
+ id: data.id,
3479
+ state: mapRunState(data.state),
3480
+ webUrl: data._links?.web?.href ?? null
3481
+ };
3482
+ }
3483
+ function buildUrl(context, buildId) {
3484
+ return withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}`));
3485
+ }
3486
+ async function getBuild(context, cred, buildId) {
3487
+ const response = await fetchWithErrors(buildUrl(context, buildId).toString(), {
3488
+ headers: authHeaders(cred)
3489
+ });
3490
+ return readJsonResponse2(response);
3491
+ }
3492
+ async function getBuildStatus(context, cred, buildId) {
3493
+ const build = await getBuild(context, cred, buildId);
3494
+ return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
3495
+ }
3496
+ async function getBuildTimeline(context, cred, buildId) {
3497
+ const url = withApiVersion(
3498
+ new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
3499
+ );
3500
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3501
+ const data = await readJsonResponse2(response);
3502
+ const records = data.records ?? [];
3503
+ const errors = [];
3504
+ const stages = [];
3505
+ const jobs = [];
3506
+ const logSteps = /* @__PURE__ */ new Map();
3507
+ for (const record of records) {
3508
+ for (const issue of record.issues ?? []) {
3509
+ if (issue.type === "error" && issue.message) {
3510
+ errors.push({ message: issue.message, source: record.name ?? null });
3511
+ }
3512
+ }
3513
+ if (record.name && record.log?.id !== void 0) {
3514
+ logSteps.set(record.log.id, record.name);
3515
+ }
3516
+ if (!record.name) continue;
3517
+ const status2 = {
3518
+ name: record.name,
3519
+ state: record.state ?? "unknown",
3520
+ result: record.result ?? null
3521
+ };
3522
+ if (record.type === "Stage") {
3523
+ stages.push(status2);
3524
+ } else if (record.type === "Job") {
3525
+ jobs.push({ startTime: record.startTime, status: status2 });
3526
+ }
3527
+ }
3528
+ jobs.sort((a, b) => {
3529
+ if (a.startTime === b.startTime) return 0;
3530
+ if (a.startTime === void 0) return 1;
3531
+ if (b.startTime === void 0) return -1;
3532
+ return a.startTime < b.startTime ? -1 : 1;
3533
+ });
3534
+ return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
3535
+ }
3536
+ async function listTestRuns(context, cred, buildId) {
3537
+ const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
3538
+ url.searchParams.set("buildUri", `vstfs:///Build/Build/${buildId}`);
3539
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3540
+ const data = await readJsonResponse2(response);
3541
+ return data.value;
3542
+ }
3543
+ async function getTestSummary(context, cred, buildId) {
3544
+ const testRuns = await listTestRuns(context, cred, buildId);
3545
+ let total = 0;
3546
+ let failed = 0;
3547
+ for (const run of testRuns) {
3548
+ const runTotal = run.totalTests ?? 0;
3549
+ total += runTotal;
3550
+ const passedOrSkipped = (run.passedTests ?? 0) + (run.notApplicableTests ?? 0) + (run.incompleteTests ?? 0);
3551
+ failed += Math.max(0, runTotal - passedOrSkipped);
3552
+ }
3553
+ if (total === 0) {
3554
+ return { present: false, total: 0, failed: 0, failedTests: [] };
3555
+ }
3556
+ return { present: true, total, failed, failedTests: [] };
3557
+ }
3558
+ var MAX_FAILED_TESTS = 50;
3559
+ async function getFailedTests(context, cred, buildId) {
3560
+ const testRuns = await listTestRuns(context, cred, buildId);
3561
+ const failed = [];
3562
+ for (const testRun of testRuns) {
3563
+ if (failed.length >= MAX_FAILED_TESTS) break;
3564
+ const resultsUrl = withApiVersion(
3565
+ new URL(`${orgProjectBase(context)}/_apis/test/runs/${testRun.id}/results`)
3566
+ );
3567
+ resultsUrl.searchParams.set("outcomes", "Failed");
3568
+ resultsUrl.searchParams.set("$top", String(MAX_FAILED_TESTS - failed.length));
3569
+ const resultsResponse = await fetchWithErrors(resultsUrl.toString(), {
3570
+ headers: authHeaders(cred)
3571
+ });
3572
+ const resultsData = await readJsonResponse2(resultsResponse);
3573
+ for (const result of resultsData.value) {
3574
+ failed.push({
3575
+ name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
3576
+ errorMessage: result.errorMessage ?? null
3577
+ });
3578
+ }
3579
+ }
3580
+ return failed;
3581
+ }
3582
+ function secondsBetween(start, finish) {
3583
+ if (!start || !finish) return null;
3584
+ const ms = Date.parse(finish) - Date.parse(start);
3585
+ return Number.isFinite(ms) ? Math.round(ms / 1e3) : null;
3586
+ }
3587
+ async function getRunDetail(context, cred, buildId) {
3588
+ const build = await getBuild(context, cred, buildId);
3589
+ let errors = [];
3590
+ let stages = [];
3591
+ let jobs = [];
3592
+ let errorsAvailable = true;
3593
+ try {
3594
+ const timeline = await getBuildTimeline(context, cred, buildId);
3595
+ errors = timeline.errors;
3596
+ stages = timeline.stages;
3597
+ jobs = timeline.jobs;
3598
+ } catch {
3599
+ errorsAvailable = false;
3600
+ }
3601
+ let tests = { present: false, total: 0, failed: 0, failedTests: [] };
3602
+ let testsAvailable = true;
3603
+ try {
3604
+ tests = await getTestSummary(context, cred, buildId);
3605
+ if (tests.failed > 0) {
3606
+ try {
3607
+ tests = { ...tests, failedTests: await getFailedTests(context, cred, buildId) };
3608
+ } catch {
3609
+ }
3610
+ }
3611
+ } catch {
3612
+ testsAvailable = false;
3613
+ }
3614
+ return {
3615
+ id: build.id,
3616
+ name: build.buildNumber ?? null,
3617
+ state: mapBuildState(build.status),
3618
+ result: mapRunResult(build.result),
3619
+ // createdDate is the queue time, matching the run-list mapping.
3620
+ createdDate: build.queueTime ?? build.startTime ?? null,
3621
+ startedDate: build.startTime ?? null,
3622
+ finishedDate: build.finishTime ?? null,
3623
+ durationSeconds: secondsBetween(build.startTime, build.finishTime),
3624
+ reason: build.reason ?? null,
3625
+ requestedFor: build.requestedFor?.displayName ?? null,
3626
+ sourceBranch: build.sourceBranch ?? null,
3627
+ sourceCommit: build.sourceVersion ?? null,
3628
+ webUrl: build._links?.web?.href ?? null,
3629
+ errors,
3630
+ errorsAvailable,
3631
+ stages,
3632
+ jobs,
3633
+ tests,
3634
+ testsAvailable
3635
+ };
3636
+ }
3637
+ async function getRunLogs(context, cred, buildId) {
3638
+ const url = withApiVersion(
3639
+ new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs`)
3640
+ );
3641
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3642
+ const data = await readJsonResponse2(response);
3643
+ let logSteps = /* @__PURE__ */ new Map();
3644
+ try {
3645
+ logSteps = (await getBuildTimeline(context, cred, buildId)).logSteps;
3646
+ } catch {
3647
+ }
3648
+ return data.value.map((log) => ({
3649
+ id: log.id,
3650
+ createdOn: log.createdOn ?? null,
3651
+ lineCount: log.lineCount ?? null,
3652
+ step: logSteps.get(log.id) ?? null
3653
+ }));
3654
+ }
3655
+ async function getRunLog(context, cred, buildId, logId) {
3656
+ const url = withApiVersion(
3657
+ new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs/${logId}`)
3658
+ );
3659
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
3660
+ if (!response.ok) {
3661
+ throw new Error(`HTTP_${response.status}`);
3662
+ }
3663
+ return response.text();
3664
+ }
3665
+
3666
+ // src/commands/pipeline.ts
3667
+ var EXIT_FAILED = 1;
3668
+ var EXIT_CANCELED = 2;
3669
+ var EXIT_TIMEOUT = 124;
3226
3670
  function writeError2(message) {
3227
3671
  process.stderr.write(`Error: ${message}
3672
+ `);
3673
+ process.exitCode = 1;
3674
+ }
3675
+ function handlePipelineError(err, context) {
3676
+ const error = err instanceof Error ? err : new Error(String(err));
3677
+ if (error.message === "AUTH_FAILED") {
3678
+ writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
3679
+ return;
3680
+ }
3681
+ if (error.message === "PERMISSION_DENIED") {
3682
+ writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
3683
+ return;
3684
+ }
3685
+ if (error.message === "NETWORK_ERROR") {
3686
+ writeError2("Could not connect to Azure DevOps. Check your network connection.");
3687
+ return;
3688
+ }
3689
+ if (error.message.startsWith("NOT_FOUND")) {
3690
+ writeError2(`Resource not found in ${context?.org}/${context?.project}.`);
3691
+ return;
3692
+ }
3693
+ if (error.message.startsWith("HTTP_")) {
3694
+ writeError2(`Azure DevOps request failed with ${error.message}.`);
3695
+ return;
3696
+ }
3697
+ writeError2(error.message);
3698
+ }
3699
+ function parsePositiveId(raw) {
3700
+ if (!/^\d+$/.test(raw)) return null;
3701
+ const n = Number.parseInt(raw, 10);
3702
+ return Number.isFinite(n) && n > 0 ? n : null;
3703
+ }
3704
+ function parseOptionalCount(value, flag) {
3705
+ if (value === void 0) return void 0;
3706
+ const parsed = parsePositiveId(value);
3707
+ if (parsed === null) {
3708
+ writeError2(`Invalid ${flag} "${value}"; expected a positive integer.`);
3709
+ return null;
3710
+ }
3711
+ return parsed;
3712
+ }
3713
+ function formatBranchName2(refName) {
3714
+ if (!refName) return "\u2014";
3715
+ return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
3716
+ }
3717
+ async function resolvePipelineContext(options) {
3718
+ const context = resolveContext(options);
3719
+ const cred = await requireAuthCredential(context.org);
3720
+ return { context, cred };
3721
+ }
3722
+ function sleep(ms) {
3723
+ return new Promise((resolve2) => {
3724
+ setTimeout(resolve2, ms);
3725
+ });
3726
+ }
3727
+ function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
3728
+ const widths = [];
3729
+ for (const row of rows) {
3730
+ row.forEach((cell, i) => {
3731
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
3732
+ });
3733
+ }
3734
+ return rows.map(
3735
+ (row) => row.map((cell, i) => rightAlign.has(i) ? cell.padStart(widths[i]) : cell.padEnd(widths[i])).join(" ").trimEnd()
3736
+ ).join("\n");
3737
+ }
3738
+ function createPipelineListCommand() {
3739
+ const command = new Command13("list");
3740
+ 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) => {
3741
+ validateOrgProjectPair(options);
3742
+ let context;
3743
+ try {
3744
+ const resolved = await resolvePipelineContext(options);
3745
+ context = resolved.context;
3746
+ let definitions = await getPipelineDefinitions(resolved.context, resolved.cred);
3747
+ if (options.filter) {
3748
+ const needle = options.filter.toLowerCase();
3749
+ definitions = definitions.filter((d) => d.name.toLowerCase().includes(needle));
3750
+ }
3751
+ if (options.json) {
3752
+ process.stdout.write(`${JSON.stringify(definitions, null, 2)}
3753
+ `);
3754
+ return;
3755
+ }
3756
+ if (definitions.length === 0) {
3757
+ process.stdout.write("No pipelines found.\n");
3758
+ return;
3759
+ }
3760
+ const hasFolder = definitions.some((d) => d.folder);
3761
+ const rows = definitions.map(
3762
+ (d) => hasFolder ? [String(d.id), d.name, d.folder ?? ""] : [String(d.id), d.name]
3763
+ );
3764
+ process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
3765
+ `);
3766
+ } catch (err) {
3767
+ handlePipelineError(err, context);
3768
+ }
3769
+ });
3770
+ return command;
3771
+ }
3772
+ function runRow(run) {
3773
+ const status2 = run.result ? `${run.state}/${run.result}` : run.state;
3774
+ return [
3775
+ String(run.id),
3776
+ `[${status2}]`,
3777
+ run.createdDate ?? "\u2014",
3778
+ formatBranchName2(run.sourceBranch),
3779
+ run.sourceCommit ? run.sourceCommit.slice(0, 8) : "\u2014"
3780
+ ];
3781
+ }
3782
+ var COMMIT_SHA_PATTERN = /^[0-9a-f]{6,40}$/i;
3783
+ function parseGetRunsInputs(defIdRaw, options) {
3784
+ let defId;
3785
+ if (defIdRaw !== void 0) {
3786
+ const parsed = parsePositiveId(defIdRaw);
3787
+ if (parsed === null) {
3788
+ writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
3789
+ return null;
3790
+ }
3791
+ defId = parsed;
3792
+ } else if (options.commit === void 0 && options.pr === void 0) {
3793
+ writeError2("Definition id is required unless --commit or --pr is given.");
3794
+ return null;
3795
+ }
3796
+ const limit = parseOptionalCount(options.limit, "--limit");
3797
+ if (limit === null) return null;
3798
+ const prNumber = parseOptionalCount(options.pr, "--pr");
3799
+ if (prNumber === null) return null;
3800
+ if (options.commit !== void 0 && !COMMIT_SHA_PATTERN.test(options.commit)) {
3801
+ writeError2(`Invalid --commit "${options.commit}"; expected 6-40 hex characters.`);
3802
+ return null;
3803
+ }
3804
+ if (options.branch !== void 0 && prNumber !== void 0) {
3805
+ writeError2("Use either --branch or --pr, not both.");
3806
+ return null;
3807
+ }
3808
+ return { defId, limit: limit ?? 10, prNumber };
3809
+ }
3810
+ function createPipelineGetRunsCommand() {
3811
+ const command = new Command13("get-runs");
3812
+ 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) => {
3813
+ validateOrgProjectPair(options);
3814
+ const inputs = parseGetRunsInputs(defIdRaw, options);
3815
+ if (inputs === null) {
3816
+ return;
3817
+ }
3818
+ let context;
3819
+ try {
3820
+ const resolved = await resolvePipelineContext(options);
3821
+ context = resolved.context;
3822
+ const runs = await getPipelineRuns(resolved.context, resolved.cred, {
3823
+ definitionId: inputs.defId,
3824
+ branch: options.branch,
3825
+ prNumber: inputs.prNumber,
3826
+ commit: options.commit,
3827
+ top: inputs.limit
3828
+ });
3829
+ if (options.json) {
3830
+ process.stdout.write(`${JSON.stringify(runs, null, 2)}
3831
+ `);
3832
+ return;
3833
+ }
3834
+ if (runs.length === 0) {
3835
+ process.stdout.write(
3836
+ inputs.defId === void 0 ? "No runs found matching the filters.\n" : `No runs found for pipeline ${inputs.defId}.
3837
+ `
3838
+ );
3839
+ return;
3840
+ }
3841
+ process.stdout.write(`${formatTable(runs.map(runRow), /* @__PURE__ */ new Set([0]))}
3842
+ `);
3843
+ } catch (err) {
3844
+ handlePipelineError(err, context);
3845
+ }
3846
+ });
3847
+ return command;
3848
+ }
3849
+ function applyWaitExitCode(result) {
3850
+ if (result.timedOut) {
3851
+ process.exitCode = EXIT_TIMEOUT;
3852
+ return;
3853
+ }
3854
+ switch (result.result) {
3855
+ case "succeeded":
3856
+ return;
3857
+ case "canceled":
3858
+ process.exitCode = EXIT_CANCELED;
3859
+ return;
3860
+ case "failed":
3861
+ default:
3862
+ process.exitCode = EXIT_FAILED;
3863
+ }
3864
+ }
3865
+ function createPipelineWaitCommand() {
3866
+ const command = new Command13("wait");
3867
+ 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) => {
3868
+ validateOrgProjectPair(options);
3869
+ const runId = parsePositiveId(runIdRaw);
3870
+ if (runId === null) {
3871
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
3872
+ return;
3873
+ }
3874
+ const timeoutSec = options.timeout === void 0 ? 1800 : Number(options.timeout);
3875
+ const pollSec = options.pollInterval === void 0 ? 5 : Number(options.pollInterval);
3876
+ if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
3877
+ writeError2(`Invalid --timeout "${options.timeout}"; expected a non-negative number.`);
3878
+ return;
3879
+ }
3880
+ if (!Number.isFinite(pollSec) || pollSec <= 0) {
3881
+ writeError2(`Invalid --poll-interval "${options.pollInterval}"; expected a positive number.`);
3882
+ return;
3883
+ }
3884
+ let context;
3885
+ try {
3886
+ const resolved = await resolvePipelineContext(options);
3887
+ context = resolved.context;
3888
+ const deadline = Date.now() + timeoutSec * 1e3;
3889
+ let waitResult = null;
3890
+ for (; ; ) {
3891
+ const status2 = await getBuildStatus(resolved.context, resolved.cred, runId);
3892
+ if (status2.state === "completed") {
3893
+ waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: false };
3894
+ break;
3895
+ }
3896
+ if (Date.now() >= deadline) {
3897
+ waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: true };
3898
+ break;
3899
+ }
3900
+ await sleep(pollSec * 1e3);
3901
+ }
3902
+ applyWaitExitCode(waitResult);
3903
+ if (options.json) {
3904
+ process.stdout.write(`${JSON.stringify(waitResult, null, 2)}
3905
+ `);
3906
+ return;
3907
+ }
3908
+ if (waitResult.timedOut) {
3909
+ process.stdout.write(`Run ${runId} did not finish within ${timeoutSec}s (still ${waitResult.state}).
3910
+ `);
3911
+ } else {
3912
+ process.stdout.write(`Run ${runId} finished: ${waitResult.result ?? waitResult.state}.
3913
+ `);
3914
+ }
3915
+ } catch (err) {
3916
+ handlePipelineError(err, context);
3917
+ }
3918
+ });
3919
+ return command;
3920
+ }
3921
+ function timelineRows(items, available) {
3922
+ if (!available) {
3923
+ return [" unavailable"];
3924
+ }
3925
+ if (items.length === 0) {
3926
+ return [" (none)"];
3927
+ }
3928
+ return items.map((item) => ` - ${item.name} [${item.result ?? item.state}]`);
3929
+ }
3930
+ function formatDuration(totalSeconds) {
3931
+ const h = Math.floor(totalSeconds / 3600);
3932
+ const m = Math.floor(totalSeconds % 3600 / 60);
3933
+ const s = totalSeconds % 60;
3934
+ if (h > 0) return `${h}h${m}m${s}s`;
3935
+ if (m > 0) return `${m}m${s}s`;
3936
+ return `${s}s`;
3937
+ }
3938
+ function errorRows(detail) {
3939
+ if (!detail.errorsAvailable) {
3940
+ return [" unavailable"];
3941
+ }
3942
+ if (detail.errors.length === 0) {
3943
+ return [" (none)"];
3944
+ }
3945
+ return detail.errors.map((error) => {
3946
+ const source = error.source ? `[${error.source}] ` : "";
3947
+ return ` - ${source}${error.message}`;
3948
+ });
3949
+ }
3950
+ function failedTestRow(test) {
3951
+ if (!test.errorMessage) {
3952
+ return ` - ${test.name}`;
3953
+ }
3954
+ const firstLine = test.errorMessage.split("\n", 1)[0].trim();
3955
+ return ` - ${test.name}: ${firstLine}`;
3956
+ }
3957
+ function testRows(detail) {
3958
+ if (!detail.testsAvailable) {
3959
+ return [" unavailable"];
3960
+ }
3961
+ if (detail.tests.present) {
3962
+ return [
3963
+ ` ${detail.tests.failed} failing of ${detail.tests.total}`,
3964
+ ...detail.tests.failedTests.map(failedTestRow)
3965
+ ];
3966
+ }
3967
+ return [" no tests present"];
3968
+ }
3969
+ function formatRunDetail(detail) {
3970
+ const status2 = detail.result ? `${detail.state}/${detail.result}` : detail.state;
3971
+ const name = detail.name ? ` ${detail.name}` : "";
3972
+ const duration = detail.durationSeconds == null ? "\u2014" : formatDuration(detail.durationSeconds);
3973
+ return [
3974
+ `Run #${detail.id} [${status2}]${name}`,
3975
+ `Queued: ${detail.createdDate ?? "\u2014"} Started: ${detail.startedDate ?? "\u2014"} Finished: ${detail.finishedDate ?? "\u2014"}`,
3976
+ `Duration: ${duration} Reason: ${detail.reason ?? "\u2014"} Requested for: ${detail.requestedFor ?? "\u2014"}`,
3977
+ `Branch: ${formatBranchName2(detail.sourceBranch)} Commit: ${detail.sourceCommit ?? "unavailable"}`,
3978
+ ...detail.webUrl ? [`Link: ${detail.webUrl}`] : [],
3979
+ "",
3980
+ "Stages:",
3981
+ ...timelineRows(detail.stages, detail.errorsAvailable),
3982
+ "",
3983
+ "Jobs:",
3984
+ ...timelineRows(detail.jobs, detail.errorsAvailable),
3985
+ "",
3986
+ "Errors:",
3987
+ ...errorRows(detail),
3988
+ "",
3989
+ "Tests:",
3990
+ ...testRows(detail)
3991
+ ].join("\n");
3992
+ }
3993
+ function createPipelineGetRunDetailCommand() {
3994
+ const command = new Command13("get-run-detail");
3995
+ 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) => {
3996
+ validateOrgProjectPair(options);
3997
+ const runId = parsePositiveId(runIdRaw);
3998
+ if (runId === null) {
3999
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
4000
+ return;
4001
+ }
4002
+ let context;
4003
+ try {
4004
+ const resolved = await resolvePipelineContext(options);
4005
+ context = resolved.context;
4006
+ const detail = await getRunDetail(resolved.context, resolved.cred, runId);
4007
+ if (options.json) {
4008
+ process.stdout.write(`${JSON.stringify(detail, null, 2)}
4009
+ `);
4010
+ return;
4011
+ }
4012
+ process.stdout.write(`${formatRunDetail(detail)}
4013
+ `);
4014
+ } catch (err) {
4015
+ handlePipelineError(err, context);
4016
+ }
4017
+ });
4018
+ return command;
4019
+ }
4020
+ function grepWithContext(lines, grep, context) {
4021
+ const include = /* @__PURE__ */ new Set();
4022
+ lines.forEach((line, i) => {
4023
+ if (grep.test(line)) {
4024
+ for (let j = Math.max(0, i - context); j <= Math.min(lines.length - 1, i + context); j++) {
4025
+ include.add(j);
4026
+ }
4027
+ }
4028
+ });
4029
+ const selected = [];
4030
+ let prev = -1;
4031
+ for (const i of [...include].sort((a, b) => a - b)) {
4032
+ if (selected.length > 0 && i > prev + 1) {
4033
+ selected.push("--");
4034
+ }
4035
+ selected.push(lines[i]);
4036
+ prev = i;
4037
+ }
4038
+ return selected;
4039
+ }
4040
+ function filterLogLines(content, grep, tail, context) {
4041
+ let lines = content.split("\n");
4042
+ if (lines.at(-1) === "") {
4043
+ lines.pop();
4044
+ }
4045
+ if (grep) {
4046
+ lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
4047
+ }
4048
+ if (tail !== void 0 && lines.length > tail) {
4049
+ lines = lines.slice(-tail);
4050
+ }
4051
+ return lines;
4052
+ }
4053
+ function parseLogFilters(options) {
4054
+ if (options.logId !== void 0 && options.step !== void 0) {
4055
+ writeError2("Use either --log-id or --step, not both.");
4056
+ return null;
4057
+ }
4058
+ const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
4059
+ const slices = options.tail !== void 0 || options.grep !== void 0 || options.context !== void 0;
4060
+ if (slices && !selectsSingleLog) {
4061
+ writeError2("--tail, --grep, and --context require --log-id or --step.");
4062
+ return null;
4063
+ }
4064
+ if (options.context !== void 0 && options.grep === void 0) {
4065
+ writeError2("--context requires --grep.");
4066
+ return null;
4067
+ }
4068
+ const tail = parseOptionalCount(options.tail, "--tail");
4069
+ if (tail === null) return null;
4070
+ const contextLines = parseOptionalCount(options.context, "--context");
4071
+ if (contextLines === null) return null;
4072
+ let grep;
4073
+ if (options.grep !== void 0) {
4074
+ try {
4075
+ grep = new RegExp(options.grep);
4076
+ } catch {
4077
+ writeError2(`Invalid --grep "${options.grep}"; expected a valid regular expression.`);
4078
+ return null;
4079
+ }
4080
+ }
4081
+ return { tail, contextLines: contextLines ?? 0, grep };
4082
+ }
4083
+ function chooseStepLog(logs, step, runId) {
4084
+ const needle = step.toLowerCase();
4085
+ const matches = logs.filter((l) => l.step?.toLowerCase().includes(needle));
4086
+ const exact = matches.filter((l) => l.step?.toLowerCase() === needle);
4087
+ const chosen = exact.length === 1 ? exact : matches;
4088
+ if (chosen.length === 0) {
4089
+ writeError2(`No log matches step "${step}" in run ${runId}.`);
4090
+ return null;
4091
+ }
4092
+ if (chosen.length > 1) {
4093
+ const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
4094
+ writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
4095
+ return null;
4096
+ }
4097
+ return chosen[0].id;
4098
+ }
4099
+ async function resolveRequestedLogId(resolved, runId, options) {
4100
+ if (options.logId !== void 0) {
4101
+ const parsed = parsePositiveId(options.logId);
4102
+ if (parsed === null) {
4103
+ writeError2(`Invalid --log-id "${options.logId}"; expected a positive integer.`);
4104
+ return null;
4105
+ }
4106
+ return parsed;
4107
+ }
4108
+ if (options.step === void 0) {
4109
+ return void 0;
4110
+ }
4111
+ const allLogs = await getRunLogs(resolved.context, resolved.cred, runId);
4112
+ return chooseStepLog(allLogs, options.step, runId);
4113
+ }
4114
+ function printSingleLog(content, filters) {
4115
+ if (filters.grep !== void 0 || filters.tail !== void 0) {
4116
+ const lines = filterLogLines(content, filters.grep, filters.tail, filters.contextLines);
4117
+ if (lines.length > 0) {
4118
+ process.stdout.write(`${lines.join("\n")}
4119
+ `);
4120
+ }
4121
+ return;
4122
+ }
4123
+ process.stdout.write(content.endsWith("\n") ? content : `${content}
4124
+ `);
4125
+ }
4126
+ function createPipelineLogsCommand() {
4127
+ const command = new Command13("logs");
4128
+ 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) => {
4129
+ validateOrgProjectPair(options);
4130
+ const runId = parsePositiveId(runIdRaw);
4131
+ if (runId === null) {
4132
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
4133
+ return;
4134
+ }
4135
+ const filters = parseLogFilters(options);
4136
+ if (filters === null) {
4137
+ return;
4138
+ }
4139
+ let context;
4140
+ try {
4141
+ const resolved = await resolvePipelineContext(options);
4142
+ context = resolved.context;
4143
+ const logId = await resolveRequestedLogId(resolved, runId, options);
4144
+ if (logId === null) {
4145
+ return;
4146
+ }
4147
+ if (logId !== void 0) {
4148
+ const content = await getRunLog(resolved.context, resolved.cred, runId, logId);
4149
+ printSingleLog(content, filters);
4150
+ return;
4151
+ }
4152
+ const logs = await getRunLogs(resolved.context, resolved.cred, runId);
4153
+ if (options.json) {
4154
+ process.stdout.write(`${JSON.stringify(logs, null, 2)}
4155
+ `);
4156
+ return;
4157
+ }
4158
+ if (logs.length === 0) {
4159
+ process.stdout.write(`No logs found for run ${runId}.
4160
+ `);
4161
+ return;
4162
+ }
4163
+ const rows = logs.map((l) => [
4164
+ String(l.id),
4165
+ l.createdOn ?? "\u2014",
4166
+ l.lineCount == null ? "" : `${l.lineCount} lines`,
4167
+ l.step ?? ""
4168
+ ]);
4169
+ process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
4170
+ `);
4171
+ } catch (err) {
4172
+ handlePipelineError(err, context);
4173
+ }
4174
+ });
4175
+ return command;
4176
+ }
4177
+ function createPipelineTestsCommand() {
4178
+ const command = new Command13("tests");
4179
+ 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) => {
4180
+ validateOrgProjectPair(options);
4181
+ const runId = parsePositiveId(runIdRaw);
4182
+ if (runId === null) {
4183
+ writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
4184
+ return;
4185
+ }
4186
+ let context;
4187
+ try {
4188
+ const resolved = await resolvePipelineContext(options);
4189
+ context = resolved.context;
4190
+ const summary = await getTestSummary(resolved.context, resolved.cred, runId);
4191
+ const failedTests = summary.failed > 0 ? await getFailedTests(resolved.context, resolved.cred, runId) : [];
4192
+ if (options.json) {
4193
+ process.stdout.write(`${JSON.stringify({ ...summary, failedTests }, null, 2)}
4194
+ `);
4195
+ return;
4196
+ }
4197
+ if (!summary.present) {
4198
+ process.stdout.write(`No test results published for run ${runId}.
4199
+ `);
4200
+ return;
4201
+ }
4202
+ if (!options.failed) {
4203
+ process.stdout.write(`Run #${runId}: ${summary.failed} failing of ${summary.total} tests
4204
+ `);
4205
+ }
4206
+ if (failedTests.length > 0) {
4207
+ process.stdout.write(`${failedTests.map(failedTestRow).join("\n")}
4208
+ `);
4209
+ } else if (options.failed) {
4210
+ process.stdout.write("No failing tests.\n");
4211
+ }
4212
+ } catch (err) {
4213
+ handlePipelineError(err, context);
4214
+ }
4215
+ });
4216
+ return command;
4217
+ }
4218
+ function parseParameters(values) {
4219
+ const result = {};
4220
+ for (const entry of values ?? []) {
4221
+ const eq = entry.indexOf("=");
4222
+ if (eq <= 0) {
4223
+ return null;
4224
+ }
4225
+ const key = entry.slice(0, eq);
4226
+ const value = entry.slice(eq + 1);
4227
+ result[key] = value;
4228
+ }
4229
+ return result;
4230
+ }
4231
+ function collectParameter(value, previous) {
4232
+ return previous.concat([value]);
4233
+ }
4234
+ function createPipelineStartCommand() {
4235
+ const command = new Command13("start");
4236
+ 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) => {
4237
+ validateOrgProjectPair(options);
4238
+ const defId = parsePositiveId(defIdRaw);
4239
+ if (defId === null) {
4240
+ writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
4241
+ return;
4242
+ }
4243
+ const parameters = parseParameters(options.parameter);
4244
+ if (parameters === null) {
4245
+ writeError2("Invalid --parameter; expected key=value.");
4246
+ return;
4247
+ }
4248
+ let context;
4249
+ try {
4250
+ const resolved = await resolvePipelineContext(options);
4251
+ context = resolved.context;
4252
+ const result = await runPipeline(resolved.context, resolved.cred, defId, {
4253
+ branch: options.branch,
4254
+ parameters
4255
+ });
4256
+ if (options.json) {
4257
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4258
+ `);
4259
+ return;
4260
+ }
4261
+ process.stdout.write(`Queued run #${result.id} [${result.state}]
4262
+ ${result.webUrl ?? "\u2014"}
4263
+ `);
4264
+ } catch (err) {
4265
+ handlePipelineError(err, context);
4266
+ }
4267
+ });
4268
+ return command;
4269
+ }
4270
+ function createPipelineCommand() {
4271
+ const command = new Command13("pipeline");
4272
+ command.description("Manage Azure DevOps pipelines");
4273
+ command.addCommand(createPipelineListCommand());
4274
+ command.addCommand(createPipelineGetRunsCommand());
4275
+ command.addCommand(createPipelineWaitCommand());
4276
+ command.addCommand(createPipelineGetRunDetailCommand());
4277
+ command.addCommand(createPipelineLogsCommand());
4278
+ command.addCommand(createPipelineTestsCommand());
4279
+ command.addCommand(createPipelineStartCommand());
4280
+ return command;
4281
+ }
4282
+
4283
+ // src/commands/comments.ts
4284
+ import { Command as Command14 } from "commander";
4285
+ function writeError3(message) {
4286
+ process.stderr.write(`Error: ${message}
3228
4287
  `);
3229
4288
  process.exit(1);
3230
4289
  }
@@ -3242,7 +4301,7 @@ function formatComments(result, convertMarkdown) {
3242
4301
  return lines.join("\n");
3243
4302
  }
3244
4303
  function createCommentsListCommand() {
3245
- const command = new Command13("list");
4304
+ const command = new Command14("list");
3246
4305
  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) => {
3247
4306
  validateOrgProjectPair(options);
3248
4307
  const id = parseWorkItemId(idStr);
@@ -3270,12 +4329,12 @@ function createCommentsListCommand() {
3270
4329
  return command;
3271
4330
  }
3272
4331
  function createCommentsAddCommand() {
3273
- const command = new Command13("add");
4332
+ const command = new Command14("add");
3274
4333
  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) => {
3275
4334
  validateOrgProjectPair(options);
3276
4335
  const id = parseWorkItemId(idStr);
3277
4336
  if (text.trim() === "") {
3278
- writeError2("Comment text must be a non-empty string.");
4337
+ writeError3("Comment text must be a non-empty string.");
3279
4338
  }
3280
4339
  let context;
3281
4340
  try {
@@ -3297,7 +4356,7 @@ function createCommentsAddCommand() {
3297
4356
  return command;
3298
4357
  }
3299
4358
  function createCommentsCommand() {
3300
- const command = new Command13("comments");
4359
+ const command = new Command14("comments");
3301
4360
  command.description("Manage Azure DevOps work item comments");
3302
4361
  command.addCommand(createCommentsListCommand());
3303
4362
  command.addCommand(createCommentsAddCommand());
@@ -3305,12 +4364,12 @@ function createCommentsCommand() {
3305
4364
  }
3306
4365
 
3307
4366
  // src/commands/download-attachment.ts
3308
- import { Command as Command14 } from "commander";
4367
+ import { Command as Command15 } from "commander";
3309
4368
  import { writeFile as writeFile2 } from "fs/promises";
3310
4369
  import { existsSync as existsSync5 } from "fs";
3311
4370
  import { join as join3 } from "path";
3312
4371
  function createDownloadAttachmentCommand() {
3313
- const command = new Command14("download-attachment");
4372
+ const command = new Command15("download-attachment");
3314
4373
  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(
3315
4374
  async (idStr, filename, options) => {
3316
4375
  const id = parseWorkItemId(idStr);
@@ -3456,7 +4515,15 @@ async function getUpdateNotice(opts) {
3456
4515
  }
3457
4516
 
3458
4517
  // src/index.ts
3459
- var program = new Command15();
4518
+ function exitOnEpipe(err) {
4519
+ if (err.code === "EPIPE") {
4520
+ process.exit(0);
4521
+ }
4522
+ throw err;
4523
+ }
4524
+ process.stdout.on("error", exitOnEpipe);
4525
+ process.stderr.on("error", exitOnEpipe);
4526
+ var program = new Command16();
3460
4527
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
3461
4528
  program.option("--no-update-check", "Skip the check for a newer published version");
3462
4529
  program.addCommand(createGetItemCommand());
@@ -3471,6 +4538,7 @@ program.addCommand(createSetMdFieldCommand());
3471
4538
  program.addCommand(createUpsertCommand());
3472
4539
  program.addCommand(createListFieldsCommand());
3473
4540
  program.addCommand(createPrCommand());
4541
+ program.addCommand(createPipelineCommand());
3474
4542
  program.addCommand(createCommentsCommand());
3475
4543
  program.addCommand(createDownloadAttachmentCommand());
3476
4544
  program.showHelpAfterError();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.10.0-develop.386",
3
+ "version": "0.10.0-develop.423",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {