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

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 +2 -0
  2. package/dist/index.js +245 -213
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,6 +89,8 @@ azdo pr comment-reply 148 "Done." --pr-number 64 # flat
89
89
  # Open a pull request — description from a repo template when you don't pass one
90
90
  azdo pr open --title "Fix the thing" --description "Because X was broken"
91
91
  azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
92
+ # The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
93
+ # the command says by how much and creates nothing. See docs/commands.md.
92
94
 
93
95
  # Link/unlink a work item, add/remove reviewers
94
96
  azdo pr work-items link 1234 --pr-number 64
package/dist/index.js CHANGED
@@ -78,6 +78,22 @@ function redactUrl(url) {
78
78
  return url;
79
79
  }
80
80
  }
81
+ var SECRET_IN_TEXT = [
82
+ [/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
83
+ [
84
+ /(\b(?:access[_-]?token|refresh[_-]?token|token|pat|password|secret|api[_-]?key)\b["']?\s*[:=]\s*["']?)[^"'\s,;&}]+/gi,
85
+ `$1${REDACTED}`
86
+ ],
87
+ [/\beyJ[A-Za-z0-9._~+/=-]{20,}/g, REDACTED],
88
+ [/\b(?=[A-Za-z0-9]*\d)(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{40,}\b/g, REDACTED]
89
+ ];
90
+ function redactText(text) {
91
+ let out = text;
92
+ for (const [pattern, replacement] of SECRET_IN_TEXT) {
93
+ out = out.replace(pattern, replacement);
94
+ }
95
+ return out;
96
+ }
81
97
  function redactBody(body) {
82
98
  if (body === null) return null;
83
99
  try {
@@ -123,6 +139,131 @@ function getActiveTraceWriter() {
123
139
  return activeWriter;
124
140
  }
125
141
 
142
+ // src/services/command-helpers.ts
143
+ function isSentinel(message, sentinel) {
144
+ return message === sentinel || message.startsWith(`${sentinel}: `);
145
+ }
146
+ function withDetail(sentinel, detail) {
147
+ return detail === null || detail === "" ? sentinel : `${sentinel}: ${detail}`;
148
+ }
149
+ function sentinelDetail(message, sentinel) {
150
+ return message.startsWith(`${sentinel}: `) ? message.slice(sentinel.length + 2) : null;
151
+ }
152
+ function splitSentinel(message) {
153
+ const separator = message.indexOf(": ");
154
+ return separator === -1 ? { sentinel: message, detail: null } : { sentinel: message.slice(0, separator), detail: message.slice(separator + 2) };
155
+ }
156
+ function writeErrorDetail(message, sentinel) {
157
+ const detail = sentinelDetail(message, sentinel);
158
+ if (detail !== null) {
159
+ process.stderr.write(` ${detail}
160
+ `);
161
+ }
162
+ }
163
+ async function promptYesNo(prompt) {
164
+ if (!process.stdin.isTTY) return true;
165
+ process.stderr.write(prompt);
166
+ return await new Promise((resolve2) => {
167
+ process.stdin.setEncoding("utf8");
168
+ let answered = false;
169
+ const handler = (data) => {
170
+ if (answered) return;
171
+ answered = true;
172
+ process.stdin.removeListener("data", handler);
173
+ process.stdin.pause();
174
+ const trimmed = data.trim().toLowerCase();
175
+ resolve2(trimmed === "y" || trimmed === "yes");
176
+ };
177
+ process.stdin.resume();
178
+ process.stdin.on("data", handler);
179
+ });
180
+ }
181
+ function parseWorkItemId(idStr) {
182
+ const id = Number.parseInt(idStr, 10);
183
+ if (!Number.isInteger(id) || id <= 0) {
184
+ process.stderr.write(
185
+ `Error: Work item ID must be a positive integer. Got: "${idStr}"
186
+ `
187
+ );
188
+ process.exit(1);
189
+ }
190
+ return id;
191
+ }
192
+ function validateOrgProjectPair(options) {
193
+ const hasOrg = options.org !== void 0;
194
+ const hasProject = options.project !== void 0;
195
+ if (hasOrg !== hasProject) {
196
+ process.stderr.write(
197
+ "Error: --org and --project must both be provided, or both omitted.\n"
198
+ );
199
+ process.exit(1);
200
+ }
201
+ }
202
+ function validateSource(options) {
203
+ const hasContent = options.content !== void 0;
204
+ const hasFile = options.file !== void 0;
205
+ if (hasContent === hasFile) {
206
+ process.stderr.write("Error: provide exactly one of --content or --file\n");
207
+ process.exit(1);
208
+ }
209
+ }
210
+ function formatCreateError(err) {
211
+ const error = err instanceof Error ? err : new Error(String(err));
212
+ const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
213
+ const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
214
+ if (requiredMatches.length > 0) {
215
+ const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
216
+ return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
217
+ }
218
+ return `Create rejected: ${message}`;
219
+ }
220
+ function handleCommandError(err, id, context, scope = "write", exit = true) {
221
+ const error = err instanceof Error ? err : new Error(String(err));
222
+ const msg = error.message;
223
+ const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
224
+ if (isSentinel(msg, "AUTH_FAILED")) {
225
+ process.stderr.write(
226
+ `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
227
+ `
228
+ );
229
+ writeErrorDetail(msg, "AUTH_FAILED");
230
+ } else if (isSentinel(msg, "PERMISSION_DENIED")) {
231
+ process.stderr.write(
232
+ `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
233
+ `
234
+ );
235
+ writeErrorDetail(msg, "PERMISSION_DENIED");
236
+ } else if (msg.startsWith("NOT_FOUND")) {
237
+ process.stderr.write(
238
+ `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
239
+ `
240
+ );
241
+ } else if (msg === "NETWORK_ERROR") {
242
+ process.stderr.write(
243
+ "Error: Could not connect to Azure DevOps. Check your network connection.\n"
244
+ );
245
+ } else if (msg.startsWith("BAD_REQUEST:")) {
246
+ const serverMsg = msg.replace("BAD_REQUEST: ", "");
247
+ process.stderr.write(`Error: Request rejected: ${serverMsg}
248
+ `);
249
+ } else if (msg.startsWith("CREATE_REJECTED:")) {
250
+ process.stderr.write(`Error: ${formatCreateError(error)}
251
+ `);
252
+ } else if (msg.startsWith("UPDATE_REJECTED:")) {
253
+ const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
254
+ process.stderr.write(`Error: Update rejected: ${serverMsg}
255
+ `);
256
+ } else {
257
+ process.stderr.write(`Error: ${msg}
258
+ `);
259
+ }
260
+ if (exit) {
261
+ process.exit(1);
262
+ } else {
263
+ process.exitCode = 1;
264
+ }
265
+ }
266
+
126
267
  // src/services/image-download.ts
127
268
  import { Jimp } from "jimp";
128
269
  import { writeFile } from "fs/promises";
@@ -329,110 +470,111 @@ async function fetchRaw(url, init) {
329
470
  var MAX_DETAIL_CHARS = 500;
330
471
  var MAX_RAW_BODY_CHARS = 200;
331
472
  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;
473
+ function renderErrorFields(body, requireMessage = false) {
340
474
  let parsed;
341
475
  try {
342
- parsed = JSON.parse(redacted);
476
+ parsed = JSON.parse(body);
343
477
  } 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));
478
+ return null;
348
479
  }
349
- const record = parsed;
480
+ if (typeof parsed !== "object" || parsed === null) return null;
481
+ return renderErrorRecord(parsed, requireMessage);
482
+ }
483
+ function renderErrorRecord(record, requireMessage) {
350
484
  const parts = [];
351
- if (typeof record.message === "string" && record.message.trim() !== "") {
352
- parts.push(record.message.trim());
485
+ const message = typeof record.message === "string" ? record.message.trim() : "";
486
+ if (message !== "") {
487
+ parts.push(message);
488
+ } else if (requireMessage) {
489
+ return null;
353
490
  }
354
491
  if (typeof record.typeKey === "string" && record.typeKey.trim() !== "") {
355
492
  parts.push(`[${record.typeKey.trim()}]`);
356
493
  } else if (typeof record.errorCode === "number" || typeof record.errorCode === "string") {
357
494
  parts.push(`[errorCode ${record.errorCode}]`);
358
495
  }
359
- if (parts.length === 0) {
360
- return truncateDetail(redacted.slice(0, MAX_RAW_BODY_CHARS));
496
+ return parts.length === 0 ? null : parts.join(" ");
497
+ }
498
+ function describeFailureBody(body, contentType) {
499
+ if (body === null) return null;
500
+ const trimmed = body.trim();
501
+ if (trimmed === "") return null;
502
+ if (contentType.toLowerCase().startsWith("text/html") || trimmed.startsWith("<")) {
503
+ return null;
361
504
  }
362
- return truncateDetail(parts.join(" "));
505
+ const redacted = redactBody(trimmed) ?? trimmed;
506
+ return truncateDetail(renderErrorFields(redacted) ?? redacted.slice(0, MAX_RAW_BODY_CHARS));
363
507
  }
364
508
  function truncateDetail(detail) {
365
- const collapsed = detail.replace(/\s+/g, " ").trim();
509
+ const collapsed = redactText(detail).replace(/\s+/g, " ").trim();
366
510
  if (collapsed === "") return null;
367
511
  return collapsed.length > MAX_DETAIL_CHARS ? `${collapsed.slice(0, MAX_DETAIL_CHARS)}\u2026(truncated)` : collapsed;
368
512
  }
369
- function withDetail(sentinel, detail) {
370
- return detail === null ? sentinel : `${sentinel}: ${detail}`;
371
- }
372
513
  function httpError(response) {
373
514
  return new Error(withDetail(`HTTP_${response.status}`, failureDetails.get(response) ?? null));
374
515
  }
375
- async function fetchWithErrors(url, init) {
516
+ var failureBodies = /* @__PURE__ */ new WeakMap();
517
+ async function readFailureBody(response, tracedBody) {
518
+ if (tracedBody !== null) return tracedBody;
519
+ try {
520
+ return await response.clone().text();
521
+ } catch {
522
+ try {
523
+ return await response.text();
524
+ } catch {
525
+ return null;
526
+ }
527
+ }
528
+ }
529
+ function writeTraceEntry(writer, url, init, response, responseBody) {
530
+ const respHeaders = {};
531
+ response.headers.forEach((v, k) => {
532
+ respHeaders[k] = v;
533
+ });
534
+ const entry = {
535
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
536
+ method: (init.method ?? "GET").toUpperCase(),
537
+ url: redactUrl(url),
538
+ requestHeaders: redactHeaders(init.headers ?? {}),
539
+ requestBody: typeof init.body === "string" ? redactBody(init.body) : null,
540
+ responseStatus: response.status,
541
+ responseHeaders: respHeaders,
542
+ responseBody
543
+ };
544
+ writer.append(entry);
545
+ }
546
+ async function traceResponse(url, init, response) {
376
547
  const writer = getActiveTraceWriter();
548
+ if (!writer) return null;
549
+ let responseBody = "";
550
+ try {
551
+ responseBody = await response.clone().text();
552
+ } catch {
553
+ }
554
+ writeTraceEntry(writer, url, init, response, responseBody);
555
+ return responseBody;
556
+ }
557
+ async function fetchWithErrors(url, init) {
377
558
  let response;
378
559
  try {
379
560
  response = await fetch(url, init);
380
561
  } catch (err) {
381
562
  throw new Error("NETWORK_ERROR", { cause: err });
382
563
  }
383
- let tracedBody = null;
384
- if (writer) {
385
- const reqHeaders = redactHeaders(init.headers ?? {});
386
- const reqBody = typeof init.body === "string" ? redactBody(init.body) : null;
387
- let responseBody = "";
388
- const clone = response.clone();
389
- try {
390
- responseBody = await clone.text();
391
- } catch {
392
- }
393
- tracedBody = responseBody;
394
- const respHeaders = {};
395
- response.headers.forEach((v, k) => {
396
- respHeaders[k] = v;
397
- });
398
- const entry = {
399
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
400
- method: (init.method ?? "GET").toUpperCase(),
401
- url: redactUrl(url),
402
- requestHeaders: reqHeaders,
403
- requestBody: reqBody ?? null,
404
- responseStatus: response.status,
405
- responseHeaders: respHeaders,
406
- responseBody
407
- };
408
- writer.append(entry);
409
- }
564
+ const tracedBody = await traceResponse(url, init, response);
410
565
  const contentType = response.headers?.get("content-type") ?? "";
411
566
  let failureDetail = null;
412
567
  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
- }
568
+ const body = await readFailureBody(response, tracedBody);
569
+ if (body !== null) failureBodies.set(response, body);
421
570
  failureDetail = describeFailureBody(body, contentType);
422
- if (failureDetail !== null) {
423
- failureDetails.set(response, failureDetail);
424
- }
571
+ if (failureDetail !== null) failureDetails.set(response, failureDetail);
425
572
  }
426
573
  if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
427
574
  if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
428
575
  if (response.status === 404) {
429
- let detail = "";
430
- try {
431
- const body = tracedBody ?? await response.text();
432
- detail = ` | url=${url} | body=${body}`;
433
- } catch {
434
- }
435
- throw new Error(`NOT_FOUND${detail}`);
576
+ const body = failureBodies.get(response);
577
+ throw new Error(body === void 0 ? "NOT_FOUND" : `NOT_FOUND | url=${url} | body=${body}`);
436
578
  }
437
579
  if (contentType.toLowerCase().startsWith("text/html")) {
438
580
  throw new Error("AUTH_FAILED");
@@ -440,14 +582,18 @@ async function fetchWithErrors(url, init) {
440
582
  return response;
441
583
  }
442
584
  async function readResponseMessage(response) {
585
+ const captured = failureBodies.get(response);
586
+ const rendered = captured === void 0 ? await renderErrorStream(response) : renderErrorFields(redactBody(captured.trim()) ?? captured.trim(), true);
587
+ return rendered === null ? null : truncateDetail(rendered);
588
+ }
589
+ async function renderErrorStream(response) {
443
590
  try {
444
591
  const body = await response.json();
445
- if (typeof body.message === "string" && body.message.trim() !== "") {
446
- return body.message.trim();
447
- }
592
+ if (typeof body !== "object" || body === null) return null;
593
+ return renderErrorRecord(body, true);
448
594
  } catch {
595
+ return null;
449
596
  }
450
- return null;
451
597
  }
452
598
  function normalizeFieldList(fields) {
453
599
  return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
@@ -1609,128 +1755,6 @@ function toMarkdown(content) {
1609
1755
  return decodeEntitiesInMarkdownCodeSpans(decodeAdoEntitiesInMarkdown(content));
1610
1756
  }
1611
1757
 
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
- }
1630
- async function promptYesNo(prompt) {
1631
- if (!process.stdin.isTTY) return true;
1632
- process.stderr.write(prompt);
1633
- return await new Promise((resolve2) => {
1634
- process.stdin.setEncoding("utf8");
1635
- let answered = false;
1636
- const handler = (data) => {
1637
- if (answered) return;
1638
- answered = true;
1639
- process.stdin.removeListener("data", handler);
1640
- process.stdin.pause();
1641
- const trimmed = data.trim().toLowerCase();
1642
- resolve2(trimmed === "y" || trimmed === "yes");
1643
- };
1644
- process.stdin.resume();
1645
- process.stdin.on("data", handler);
1646
- });
1647
- }
1648
- function parseWorkItemId(idStr) {
1649
- const id = Number.parseInt(idStr, 10);
1650
- if (!Number.isInteger(id) || id <= 0) {
1651
- process.stderr.write(
1652
- `Error: Work item ID must be a positive integer. Got: "${idStr}"
1653
- `
1654
- );
1655
- process.exit(1);
1656
- }
1657
- return id;
1658
- }
1659
- function validateOrgProjectPair(options) {
1660
- const hasOrg = options.org !== void 0;
1661
- const hasProject = options.project !== void 0;
1662
- if (hasOrg !== hasProject) {
1663
- process.stderr.write(
1664
- "Error: --org and --project must both be provided, or both omitted.\n"
1665
- );
1666
- process.exit(1);
1667
- }
1668
- }
1669
- function validateSource(options) {
1670
- const hasContent = options.content !== void 0;
1671
- const hasFile = options.file !== void 0;
1672
- if (hasContent === hasFile) {
1673
- process.stderr.write("Error: provide exactly one of --content or --file\n");
1674
- process.exit(1);
1675
- }
1676
- }
1677
- function formatCreateError(err) {
1678
- const error = err instanceof Error ? err : new Error(String(err));
1679
- const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
1680
- const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
1681
- if (requiredMatches.length > 0) {
1682
- const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
1683
- return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
1684
- }
1685
- return `Create rejected: ${message}`;
1686
- }
1687
- function handleCommandError(err, id, context, scope = "write", exit = true) {
1688
- const error = err instanceof Error ? err : new Error(String(err));
1689
- const msg = error.message;
1690
- const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
1691
- if (isSentinel(msg, "AUTH_FAILED")) {
1692
- process.stderr.write(
1693
- `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
1694
- `
1695
- );
1696
- writeErrorDetail(msg, "AUTH_FAILED");
1697
- } else if (isSentinel(msg, "PERMISSION_DENIED")) {
1698
- process.stderr.write(
1699
- `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
1700
- `
1701
- );
1702
- writeErrorDetail(msg, "PERMISSION_DENIED");
1703
- } else if (msg.startsWith("NOT_FOUND")) {
1704
- process.stderr.write(
1705
- `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
1706
- `
1707
- );
1708
- } else if (msg === "NETWORK_ERROR") {
1709
- process.stderr.write(
1710
- "Error: Could not connect to Azure DevOps. Check your network connection.\n"
1711
- );
1712
- } else if (msg.startsWith("BAD_REQUEST:")) {
1713
- const serverMsg = msg.replace("BAD_REQUEST: ", "");
1714
- process.stderr.write(`Error: Request rejected: ${serverMsg}
1715
- `);
1716
- } else if (msg.startsWith("CREATE_REJECTED:")) {
1717
- process.stderr.write(`Error: ${formatCreateError(error)}
1718
- `);
1719
- } else if (msg.startsWith("UPDATE_REJECTED:")) {
1720
- const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
1721
- process.stderr.write(`Error: Update rejected: ${serverMsg}
1722
- `);
1723
- } else {
1724
- process.stderr.write(`Error: ${msg}
1725
- `);
1726
- }
1727
- if (exit) {
1728
- process.exit(1);
1729
- } else {
1730
- process.exitCode = 1;
1731
- }
1732
- }
1733
-
1734
1758
  // src/commands/get-item.ts
1735
1759
  function parseRequestedFields(raw) {
1736
1760
  if (raw === void 0) return void 0;
@@ -3655,8 +3679,11 @@ function composeDescription(description, template) {
3655
3679
  }
3656
3680
  function formatDescriptionOverflow(composed) {
3657
3681
  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.`;
3682
+ return `description is ${composed.totalChars} characters${formatContributions(composed)}, exceeding the Azure DevOps limit of ${MAX_PR_DESCRIPTION_CHARS} characters. Shorten the description by at least ${overflow} characters.`;
3683
+ }
3684
+ function formatContributions(composed) {
3685
+ if (composed.templatePath === null) return "";
3686
+ return ` (${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} from the repository pull request template ${composed.templatePath})`;
3660
3687
  }
3661
3688
  function describeDescriptionBudget(composed) {
3662
3689
  return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
@@ -3901,7 +3928,7 @@ async function resolveReviewerIdentity(org, cred, input) {
3901
3928
  });
3902
3929
  } catch (err) {
3903
3930
  if (err instanceof Error && isSentinel(err.message, "AUTH_FAILED")) {
3904
- throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
3931
+ throw new Error(withDetail("IDENTITY_SCOPE_MISSING", sentinelDetail(err.message, "AUTH_FAILED")), { cause: err });
3905
3932
  }
3906
3933
  throw err;
3907
3934
  }
@@ -4144,11 +4171,12 @@ function handlePrCommandError(err, context, mode = "read") {
4144
4171
  writeErrorDetail(error.message, "AUTH_FAILED");
4145
4172
  return;
4146
4173
  }
4147
- if (error.message === "IDENTITY_SCOPE_MISSING") {
4174
+ if (isSentinel(error.message, "IDENTITY_SCOPE_MISSING")) {
4148
4175
  writeError(
4149
4176
  'Could not resolve reviewer identity: your PAT is missing the "Identity (Read)" scope required by the Azure DevOps identities API (separate from Code scope).',
4150
4177
  EXIT_NOT_PERMITTED
4151
4178
  );
4179
+ writeErrorDetail(error.message, "IDENTITY_SCOPE_MISSING");
4152
4180
  return;
4153
4181
  }
4154
4182
  if (isSentinel(error.message, "PERMISSION_DENIED")) {
@@ -4345,6 +4373,23 @@ function createPrStatusCommand() {
4345
4373
  });
4346
4374
  return command;
4347
4375
  }
4376
+ function handlePrOpenError(err, context) {
4377
+ const message = err instanceof Error ? err.message : "";
4378
+ if (message.startsWith("AMBIGUOUS_PRS:")) {
4379
+ const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
4380
+ writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
4381
+ return;
4382
+ }
4383
+ if (message === "DESCRIPTION_REQUIRED") {
4384
+ writeError("--description is required for pull request creation.");
4385
+ return;
4386
+ }
4387
+ if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
4388
+ writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
4389
+ return;
4390
+ }
4391
+ handlePrCommandError(err, context, "write");
4392
+ }
4348
4393
  function createPrOpenCommand() {
4349
4394
  const command = new Command12("open");
4350
4395
  withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
@@ -4393,20 +4438,7 @@ ${result.pullRequest.url ?? "\u2014"}
4393
4438
  `
4394
4439
  );
4395
4440
  } catch (err) {
4396
- if (err instanceof Error && err.message.startsWith("AMBIGUOUS_PRS:")) {
4397
- const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
4398
- writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
4399
- return;
4400
- }
4401
- if (err instanceof Error && err.message === "DESCRIPTION_REQUIRED") {
4402
- writeError("--description is required for pull request creation.");
4403
- return;
4404
- }
4405
- if (err instanceof Error && err.message.startsWith("DESCRIPTION_TOO_LONG: ")) {
4406
- writeError(err.message.slice("DESCRIPTION_TOO_LONG: ".length));
4407
- return;
4408
- }
4409
- handlePrCommandError(err, context, "write");
4441
+ handlePrOpenError(err, context);
4410
4442
  }
4411
4443
  });
4412
4444
  return command;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.19.0-feature-037-api-error-surfacing.693",
3
+ "version": "0.19.0-feature-037-api-error-surfacing.695",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {