azdo-cli 0.19.0-feature-automata.685 → 0.19.0-feature-037-api-error-surfacing.693

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 (2) hide show
  1. package/dist/index.js +182 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -326,6 +326,52 @@ async function fetchRaw(url, init) {
326
326
  const body = await response.text();
327
327
  return { status: response.status, body };
328
328
  }
329
+ var MAX_DETAIL_CHARS = 500;
330
+ var MAX_RAW_BODY_CHARS = 200;
331
+ var failureDetails = /* @__PURE__ */ new WeakMap();
332
+ function describeFailureBody(body, contentType) {
333
+ if (body === null) return null;
334
+ const trimmed = body.trim();
335
+ if (trimmed === "") return null;
336
+ if (contentType.toLowerCase().startsWith("text/html") || trimmed.startsWith("<")) {
337
+ return null;
338
+ }
339
+ const redacted = redactBody(trimmed) ?? trimmed;
340
+ let parsed;
341
+ try {
342
+ parsed = JSON.parse(redacted);
343
+ } catch {
344
+ return truncateDetail(redacted.slice(0, MAX_RAW_BODY_CHARS));
345
+ }
346
+ if (typeof parsed !== "object" || parsed === null) {
347
+ return truncateDetail(redacted.slice(0, MAX_RAW_BODY_CHARS));
348
+ }
349
+ const record = parsed;
350
+ const parts = [];
351
+ if (typeof record.message === "string" && record.message.trim() !== "") {
352
+ parts.push(record.message.trim());
353
+ }
354
+ if (typeof record.typeKey === "string" && record.typeKey.trim() !== "") {
355
+ parts.push(`[${record.typeKey.trim()}]`);
356
+ } else if (typeof record.errorCode === "number" || typeof record.errorCode === "string") {
357
+ parts.push(`[errorCode ${record.errorCode}]`);
358
+ }
359
+ if (parts.length === 0) {
360
+ return truncateDetail(redacted.slice(0, MAX_RAW_BODY_CHARS));
361
+ }
362
+ return truncateDetail(parts.join(" "));
363
+ }
364
+ function truncateDetail(detail) {
365
+ const collapsed = detail.replace(/\s+/g, " ").trim();
366
+ if (collapsed === "") return null;
367
+ return collapsed.length > MAX_DETAIL_CHARS ? `${collapsed.slice(0, MAX_DETAIL_CHARS)}\u2026(truncated)` : collapsed;
368
+ }
369
+ function withDetail(sentinel, detail) {
370
+ return detail === null ? sentinel : `${sentinel}: ${detail}`;
371
+ }
372
+ function httpError(response) {
373
+ return new Error(withDetail(`HTTP_${response.status}`, failureDetails.get(response) ?? null));
374
+ }
329
375
  async function fetchWithErrors(url, init) {
330
376
  const writer = getActiveTraceWriter();
331
377
  let response;
@@ -334,6 +380,7 @@ async function fetchWithErrors(url, init) {
334
380
  } catch (err) {
335
381
  throw new Error("NETWORK_ERROR", { cause: err });
336
382
  }
383
+ let tracedBody = null;
337
384
  if (writer) {
338
385
  const reqHeaders = redactHeaders(init.headers ?? {});
339
386
  const reqBody = typeof init.body === "string" ? redactBody(init.body) : null;
@@ -343,6 +390,7 @@ async function fetchWithErrors(url, init) {
343
390
  responseBody = await clone.text();
344
391
  } catch {
345
392
  }
393
+ tracedBody = responseBody;
346
394
  const respHeaders = {};
347
395
  response.headers.forEach((v, k) => {
348
396
  respHeaders[k] = v;
@@ -359,18 +407,33 @@ async function fetchWithErrors(url, init) {
359
407
  };
360
408
  writer.append(entry);
361
409
  }
362
- if (response.status === 401) throw new Error("AUTH_FAILED");
363
- if (response.status === 403) throw new Error("PERMISSION_DENIED");
410
+ const contentType = response.headers?.get("content-type") ?? "";
411
+ let failureDetail = null;
412
+ if (!response.ok) {
413
+ let body = tracedBody;
414
+ if (body === null) {
415
+ try {
416
+ body = await response.clone().text();
417
+ } catch {
418
+ body = null;
419
+ }
420
+ }
421
+ failureDetail = describeFailureBody(body, contentType);
422
+ if (failureDetail !== null) {
423
+ failureDetails.set(response, failureDetail);
424
+ }
425
+ }
426
+ if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
427
+ if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
364
428
  if (response.status === 404) {
365
429
  let detail = "";
366
430
  try {
367
- const body = await response.text();
431
+ const body = tracedBody ?? await response.text();
368
432
  detail = ` | url=${url} | body=${body}`;
369
433
  } catch {
370
434
  }
371
435
  throw new Error(`NOT_FOUND${detail}`);
372
436
  }
373
- const contentType = response.headers?.get("content-type") ?? "";
374
437
  if (contentType.toLowerCase().startsWith("text/html")) {
375
438
  throw new Error("AUTH_FAILED");
376
439
  }
@@ -464,7 +527,7 @@ async function readWriteResponse(response, errorCode) {
464
527
  throw new Error(`${errorCode}: ${serverMessage}`);
465
528
  }
466
529
  if (!response.ok) {
467
- throw new Error(`HTTP_${response.status}`);
530
+ throw httpError(response);
468
531
  }
469
532
  const data = await response.json();
470
533
  return {
@@ -487,7 +550,7 @@ async function getWorkItemFields(context, id, cred) {
487
550
  }
488
551
  }
489
552
  if (!response.ok) {
490
- throw new Error(`HTTP_${response.status}`);
553
+ throw httpError(response);
491
554
  }
492
555
  const data = await response.json();
493
556
  return data.fields;
@@ -539,7 +602,7 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
539
602
  }
540
603
  }
541
604
  if (!response.ok) {
542
- throw new Error(`HTTP_${response.status}`);
605
+ throw httpError(response);
543
606
  }
544
607
  return await response.json();
545
608
  }
@@ -550,7 +613,7 @@ async function getOrgFieldNames(context, cred) {
550
613
  url.searchParams.set("api-version", "7.1");
551
614
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
552
615
  if (!response.ok) {
553
- throw new Error(`HTTP_${response.status}`);
616
+ throw httpError(response);
554
617
  }
555
618
  const data = await response.json();
556
619
  return (data.value ?? []).map((f) => f.referenceName);
@@ -632,7 +695,7 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
632
695
  }
633
696
  }
634
697
  if (!response.ok) {
635
- throw new Error(`HTTP_${response.status}`);
698
+ throw httpError(response);
636
699
  }
637
700
  const data = await response.json();
638
701
  const value = data.fields[fieldName];
@@ -650,7 +713,7 @@ async function listWorkItemComments(context, id, cred) {
650
713
  { headers: authHeaders(cred) }
651
714
  );
652
715
  if (!response.ok) {
653
- throw new Error(`HTTP_${response.status}`);
716
+ throw httpError(response);
654
717
  }
655
718
  const data = await response.json();
656
719
  comments.push(
@@ -680,7 +743,7 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
680
743
  throw new Error(`BAD_REQUEST: ${serverMessage}`);
681
744
  }
682
745
  if (!response.ok) {
683
- throw new Error(`HTTP_${response.status}`);
746
+ throw httpError(response);
684
747
  }
685
748
  const data = await response.json();
686
749
  return {
@@ -732,7 +795,7 @@ async function applyWorkItemPatch(context, id, cred, operations) {
732
795
  async function downloadAttachment(url, cred) {
733
796
  const response = await fetchWithErrors(url, { headers: authHeaders(cred) });
734
797
  if (!response.ok) {
735
- throw new Error(`HTTP_${response.status}`);
798
+ throw httpError(response);
736
799
  }
737
800
  return response.arrayBuffer();
738
801
  }
@@ -757,7 +820,7 @@ async function createAttachment(context, fileName, content, cred) {
757
820
  }
758
821
  }
759
822
  if (!response.ok) {
760
- throw new Error(`HTTP_${response.status}`);
823
+ throw httpError(response);
761
824
  }
762
825
  return await response.json();
763
826
  }
@@ -1547,6 +1610,23 @@ function toMarkdown(content) {
1547
1610
  }
1548
1611
 
1549
1612
  // src/services/command-helpers.ts
1613
+ function isSentinel(message, sentinel) {
1614
+ return message === sentinel || message.startsWith(`${sentinel}: `);
1615
+ }
1616
+ function sentinelDetail(message, sentinel) {
1617
+ return message.startsWith(`${sentinel}: `) ? message.slice(sentinel.length + 2) : null;
1618
+ }
1619
+ function splitSentinel(message) {
1620
+ const separator = message.indexOf(": ");
1621
+ return separator === -1 ? { sentinel: message, detail: null } : { sentinel: message.slice(0, separator), detail: message.slice(separator + 2) };
1622
+ }
1623
+ function writeErrorDetail(message, sentinel) {
1624
+ const detail = sentinelDetail(message, sentinel);
1625
+ if (detail !== null) {
1626
+ process.stderr.write(` ${detail}
1627
+ `);
1628
+ }
1629
+ }
1550
1630
  async function promptYesNo(prompt) {
1551
1631
  if (!process.stdin.isTTY) return true;
1552
1632
  process.stderr.write(prompt);
@@ -1608,16 +1688,18 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
1608
1688
  const error = err instanceof Error ? err : new Error(String(err));
1609
1689
  const msg = error.message;
1610
1690
  const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
1611
- if (msg === "AUTH_FAILED") {
1691
+ if (isSentinel(msg, "AUTH_FAILED")) {
1612
1692
  process.stderr.write(
1613
1693
  `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
1614
1694
  `
1615
1695
  );
1616
- } else if (msg === "PERMISSION_DENIED") {
1696
+ writeErrorDetail(msg, "AUTH_FAILED");
1697
+ } else if (isSentinel(msg, "PERMISSION_DENIED")) {
1617
1698
  process.stderr.write(
1618
1699
  `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
1619
1700
  `
1620
1701
  );
1702
+ writeErrorDetail(msg, "PERMISSION_DENIED");
1621
1703
  } else if (msg.startsWith("NOT_FOUND")) {
1622
1704
  process.stderr.write(
1623
1705
  `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
@@ -3132,10 +3214,10 @@ function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
3132
3214
  };
3133
3215
  }
3134
3216
  function isUpdateWriteError(err) {
3135
- return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
3217
+ return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
3136
3218
  }
3137
3219
  function isCreateWriteError(err) {
3138
- return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
3220
+ return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
3139
3221
  }
3140
3222
  function handleUpsertError(err, id, context) {
3141
3223
  if (!(err instanceof Error)) {
@@ -3465,7 +3547,7 @@ function isThreadResolved(status2) {
3465
3547
  }
3466
3548
  async function readJsonResponse(response) {
3467
3549
  if (!response.ok) {
3468
- throw new Error(`HTTP_${response.status}`);
3550
+ throw httpError(response);
3469
3551
  }
3470
3552
  return response.json();
3471
3553
  }
@@ -3552,19 +3634,32 @@ async function getPullRequestBuilds(context, cred, prId) {
3552
3634
  isBlocking: null
3553
3635
  }));
3554
3636
  }
3637
+ var MAX_PR_DESCRIPTION_CHARS = 4e3;
3638
+ var DESCRIPTION_SEPARATOR = "\n\n";
3555
3639
  function composeDescription(description, template) {
3556
- if (description !== void 0 && template !== null) {
3557
- return `${description}
3558
-
3559
- ${template.content}`;
3560
- }
3561
- if (description !== void 0) {
3562
- return description;
3563
- }
3564
- if (template !== null) {
3565
- return template.content;
3640
+ if (description === void 0 && template === null) {
3641
+ return null;
3566
3642
  }
3567
- return null;
3643
+ const provided = description ?? "";
3644
+ const templateContent = template?.content ?? "";
3645
+ const separator = description !== void 0 && template !== null ? DESCRIPTION_SEPARATOR : "";
3646
+ const text = `${provided}${separator}${templateContent}`;
3647
+ return {
3648
+ text,
3649
+ providedChars: provided.length,
3650
+ separatorChars: separator.length,
3651
+ templateChars: templateContent.length,
3652
+ templatePath: template?.path ?? null,
3653
+ totalChars: text.length
3654
+ };
3655
+ }
3656
+ function formatDescriptionOverflow(composed) {
3657
+ const overflow = composed.totalChars - MAX_PR_DESCRIPTION_CHARS;
3658
+ const breakdown = composed.templateChars > 0 && composed.providedChars > 0 ? ` (${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} from the repository pull request template ${composed.templatePath})` : composed.templateChars > 0 ? ` (all of it from the repository pull request template ${composed.templatePath})` : "";
3659
+ return `description is ${composed.totalChars} characters${breakdown}, exceeding the Azure DevOps limit of ${MAX_PR_DESCRIPTION_CHARS} characters. Shorten the description by at least ${overflow} characters.`;
3660
+ }
3661
+ function describeDescriptionBudget(composed) {
3662
+ return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
3568
3663
  }
3569
3664
  async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
3570
3665
  const existing = await listPullRequests(context, repo, cred, sourceBranch, {
@@ -3585,29 +3680,40 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
3585
3680
  const repository = await getRepository(context, repo, cred);
3586
3681
  const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
3587
3682
  const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
3588
- const finalDescription = composeDescription(description, template);
3589
- if (finalDescription === null) {
3683
+ const composed = composeDescription(description, template);
3684
+ if (composed === null) {
3590
3685
  throw new Error("DESCRIPTION_REQUIRED");
3591
3686
  }
3687
+ if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
3688
+ throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
3689
+ }
3592
3690
  const payload = {
3593
3691
  sourceRefName: `refs/heads/${sourceBranch}`,
3594
3692
  targetRefName: "refs/heads/develop",
3595
3693
  title,
3596
- description: finalDescription
3694
+ description: composed.text
3597
3695
  };
3598
3696
  const url = new URL(
3599
3697
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
3600
3698
  );
3601
3699
  url.searchParams.set("api-version", "7.1");
3602
- const response = await fetchWithErrors(url.toString(), {
3603
- method: "POST",
3604
- headers: {
3605
- ...authHeaders(cred),
3606
- "Content-Type": "application/json"
3607
- },
3608
- body: JSON.stringify(payload)
3609
- });
3610
- const data = await readJsonResponse(response);
3700
+ let data;
3701
+ try {
3702
+ const response = await fetchWithErrors(url.toString(), {
3703
+ method: "POST",
3704
+ headers: {
3705
+ ...authHeaders(cred),
3706
+ "Content-Type": "application/json"
3707
+ },
3708
+ body: JSON.stringify(payload)
3709
+ });
3710
+ data = await readJsonResponse(response);
3711
+ } catch (err) {
3712
+ if (err instanceof Error && err.message.startsWith("HTTP_400")) {
3713
+ throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
3714
+ }
3715
+ throw err;
3716
+ }
3611
3717
  return {
3612
3718
  branch: sourceBranch,
3613
3719
  targetBranch: "develop",
@@ -3745,7 +3851,7 @@ async function patchWorkItemRelations(context, cred, workItemId, operation) {
3745
3851
  body: JSON.stringify([operation])
3746
3852
  });
3747
3853
  if (!response.ok) {
3748
- throw new Error(`HTTP_${response.status}`);
3854
+ throw httpError(response);
3749
3855
  }
3750
3856
  }
3751
3857
  async function linkWorkItemToPullRequest(context, repo, cred, prId, workItemId) {
@@ -3794,7 +3900,7 @@ async function resolveReviewerIdentity(org, cred, input) {
3794
3900
  headers: authHeaders(cred)
3795
3901
  });
3796
3902
  } catch (err) {
3797
- if (err instanceof Error && err.message === "AUTH_FAILED") {
3903
+ if (err instanceof Error && isSentinel(err.message, "AUTH_FAILED")) {
3798
3904
  throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
3799
3905
  }
3800
3906
  throw err;
@@ -3853,7 +3959,7 @@ async function removePullRequestReviewer(context, repo, cred, prId, reviewerId)
3853
3959
  headers: authHeaders(cred)
3854
3960
  });
3855
3961
  if (!response.ok) {
3856
- throw new Error(`HTTP_${response.status}`);
3962
+ throw httpError(response);
3857
3963
  }
3858
3964
  return { reviewer: existing, noop: false };
3859
3965
  }
@@ -3892,7 +3998,7 @@ async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
3892
3998
  throw err;
3893
3999
  }
3894
4000
  if (!response.ok) {
3895
- throw new Error(`HTTP_${response.status}`);
4001
+ throw httpError(response);
3896
4002
  }
3897
4003
  return response.text();
3898
4004
  }
@@ -4027,7 +4133,7 @@ function writeError(message, exitCode = 1) {
4027
4133
  }
4028
4134
  function handlePrCommandError(err, context, mode = "read") {
4029
4135
  const error = err instanceof Error ? err : new Error(String(err));
4030
- if (error.message === "AUTH_FAILED") {
4136
+ if (isSentinel(error.message, "AUTH_FAILED")) {
4031
4137
  const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
4032
4138
  writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`, EXIT_NOT_PERMITTED);
4033
4139
  const credentialHint = describeResolvedCredential();
@@ -4035,6 +4141,7 @@ function handlePrCommandError(err, context, mode = "read") {
4035
4141
  process.stderr.write(` ${credentialHint}
4036
4142
  `);
4037
4143
  }
4144
+ writeErrorDetail(error.message, "AUTH_FAILED");
4038
4145
  return;
4039
4146
  }
4040
4147
  if (error.message === "IDENTITY_SCOPE_MISSING") {
@@ -4044,8 +4151,9 @@ function handlePrCommandError(err, context, mode = "read") {
4044
4151
  );
4045
4152
  return;
4046
4153
  }
4047
- if (error.message === "PERMISSION_DENIED") {
4154
+ if (isSentinel(error.message, "PERMISSION_DENIED")) {
4048
4155
  writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
4156
+ writeErrorDetail(error.message, "PERMISSION_DENIED");
4049
4157
  return;
4050
4158
  }
4051
4159
  if (error.message === "NETWORK_ERROR") {
@@ -4057,7 +4165,12 @@ function handlePrCommandError(err, context, mode = "read") {
4057
4165
  return;
4058
4166
  }
4059
4167
  if (error.message.startsWith("HTTP_")) {
4060
- writeError(`Azure DevOps request failed with ${error.message}.`);
4168
+ const { sentinel, detail } = splitSentinel(error.message);
4169
+ writeError(`Azure DevOps request failed with ${sentinel}.`);
4170
+ if (detail !== null) {
4171
+ process.stderr.write(` ${detail}
4172
+ `);
4173
+ }
4061
4174
  return;
4062
4175
  }
4063
4176
  writeError(error.message);
@@ -4289,6 +4402,10 @@ ${result.pullRequest.url ?? "\u2014"}
4289
4402
  writeError("--description is required for pull request creation.");
4290
4403
  return;
4291
4404
  }
4405
+ if (err instanceof Error && err.message.startsWith("DESCRIPTION_TOO_LONG: ")) {
4406
+ writeError(err.message.slice("DESCRIPTION_TOO_LONG: ".length));
4407
+ return;
4408
+ }
4292
4409
  handlePrCommandError(err, context, "write");
4293
4410
  }
4294
4411
  });
@@ -5098,7 +5215,7 @@ function withApiVersion(url) {
5098
5215
  }
5099
5216
  async function readJsonResponse2(response) {
5100
5217
  if (!response.ok) {
5101
- throw new Error(`HTTP_${response.status}`);
5218
+ throw httpError(response);
5102
5219
  }
5103
5220
  return await response.json();
5104
5221
  }
@@ -5374,7 +5491,7 @@ async function getRunLog(context, cred, buildId, logId) {
5374
5491
  );
5375
5492
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
5376
5493
  if (!response.ok) {
5377
- throw new Error(`HTTP_${response.status}`);
5494
+ throw httpError(response);
5378
5495
  }
5379
5496
  return response.text();
5380
5497
  }
@@ -5390,12 +5507,14 @@ function writeError2(message) {
5390
5507
  }
5391
5508
  function handlePipelineError(err, context) {
5392
5509
  const error = err instanceof Error ? err : new Error(String(err));
5393
- if (error.message === "AUTH_FAILED") {
5510
+ if (isSentinel(error.message, "AUTH_FAILED")) {
5394
5511
  writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
5512
+ writeErrorDetail(error.message, "AUTH_FAILED");
5395
5513
  return;
5396
5514
  }
5397
- if (error.message === "PERMISSION_DENIED") {
5515
+ if (isSentinel(error.message, "PERMISSION_DENIED")) {
5398
5516
  writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
5517
+ writeErrorDetail(error.message, "PERMISSION_DENIED");
5399
5518
  return;
5400
5519
  }
5401
5520
  if (error.message === "NETWORK_ERROR") {
@@ -5407,7 +5526,12 @@ function handlePipelineError(err, context) {
5407
5526
  return;
5408
5527
  }
5409
5528
  if (error.message.startsWith("HTTP_")) {
5410
- writeError2(`Azure DevOps request failed with ${error.message}.`);
5529
+ const { sentinel, detail } = splitSentinel(error.message);
5530
+ writeError2(`Azure DevOps request failed with ${sentinel}.`);
5531
+ if (detail !== null) {
5532
+ process.stderr.write(` ${detail}
5533
+ `);
5534
+ }
5411
5535
  return;
5412
5536
  }
5413
5537
  writeError2(error.message);
@@ -6299,7 +6423,7 @@ function mapRelationType(raw) {
6299
6423
  };
6300
6424
  }
6301
6425
  async function readJsonResponse3(response) {
6302
- if (!response.ok) throw new Error(`HTTP_${response.status}`);
6426
+ if (!response.ok) throw httpError(response);
6303
6427
  return await response.json();
6304
6428
  }
6305
6429
  function parseTargetId(url) {
@@ -6347,7 +6471,7 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
6347
6471
  { op: "add", path: "/relations/-", value: { rel: relType.referenceName, url: targetUrl } }
6348
6472
  ])
6349
6473
  });
6350
- if (!response.ok) throw new Error(`HTTP_${response.status}`);
6474
+ if (!response.ok) throw httpError(response);
6351
6475
  return { status: "added", type: relType.name, referenceName: relType.referenceName, id1, id2 };
6352
6476
  }
6353
6477
  async function removeWorkItemRelation(context, cred, type, id1, id2) {
@@ -6367,7 +6491,7 @@ async function removeWorkItemRelation(context, cred, type, id1, id2) {
6367
6491
  headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
6368
6492
  body: JSON.stringify([{ op: "remove", path: `/relations/${index}` }])
6369
6493
  });
6370
- if (!response.ok) throw new Error(`HTTP_${response.status}`);
6494
+ if (!response.ok) throw httpError(response);
6371
6495
  return { status: "removed", type: relType.name, referenceName: relType.referenceName, id1, id2 };
6372
6496
  }
6373
6497
  async function listWorkItemRelations(context, cred, id) {
@@ -6455,11 +6579,12 @@ function handleRelationError(err, id1) {
6455
6579
  const target = id1 !== void 0 ? id1 : "unknown";
6456
6580
  process.stderr.write(`Error: work item #${target} not found.
6457
6581
  `);
6458
- } else if (msg === "AUTH_FAILED") {
6582
+ } else if (isSentinel(msg, "AUTH_FAILED")) {
6459
6583
  process.stderr.write(
6460
6584
  `Error: authentication failed. Check your PAT has Work Items \u2192 Read & Write scope.
6461
6585
  `
6462
6586
  );
6587
+ writeErrorDetail(msg, "AUTH_FAILED");
6463
6588
  } else {
6464
6589
  process.stderr.write(`Error: ${msg}
6465
6590
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.19.0-feature-automata.685",
3
+ "version": "0.19.0-feature-037-api-error-surfacing.693",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {