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.
- package/README.md +2 -0
- package/dist/index.js +245 -213
- 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
|
|
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(
|
|
476
|
+
parsed = JSON.parse(body);
|
|
343
477
|
} catch {
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
347
|
-
return truncateDetail(redacted.slice(0, MAX_RAW_BODY_CHARS));
|
|
478
|
+
return null;
|
|
348
479
|
}
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
if (body
|
|
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
|
-
|
|
430
|
-
|
|
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
|
|
446
|
-
|
|
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
|
-
|
|
3659
|
-
|
|
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
|
|
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
|
-
|
|
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;
|