azdo-cli 0.19.0-feature-037-api-error-surfacing.692 → 0.19.0-feature-037-api-error-surfacing.697
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 +361 -205
- 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,21 @@ function redactUrl(url) {
|
|
|
78
78
|
return url;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
var SECRET_ASSIGNMENT = String.raw`["']?\s*[:=]\s*["']?)[^"'\s,;&}]+`;
|
|
82
|
+
var SECRET_IN_TEXT = [
|
|
83
|
+
[/\b(Bearer|Basic)\s+[\w.~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
84
|
+
[new RegExp(`(\\b(?:access|refresh|api)[_-]?(?:token|key)\\b${SECRET_ASSIGNMENT}`, "gi"), `$1${REDACTED}`],
|
|
85
|
+
[new RegExp(`(\\b(?:token|pat|password|secret)\\b${SECRET_ASSIGNMENT}`, "gi"), `$1${REDACTED}`],
|
|
86
|
+
[/\beyJ[A-Za-z0-9._~+/=-]{20,}/g, REDACTED],
|
|
87
|
+
[/\b(?=[A-Za-z0-9]*\d)(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{40,}\b/g, REDACTED]
|
|
88
|
+
];
|
|
89
|
+
function redactText(text) {
|
|
90
|
+
let out = text;
|
|
91
|
+
for (const [pattern, replacement] of SECRET_IN_TEXT) {
|
|
92
|
+
out = out.replace(pattern, replacement);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
81
96
|
function redactBody(body) {
|
|
82
97
|
if (body === null) return null;
|
|
83
98
|
try {
|
|
@@ -123,6 +138,131 @@ function getActiveTraceWriter() {
|
|
|
123
138
|
return activeWriter;
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
// src/services/command-helpers.ts
|
|
142
|
+
function isSentinel(message, sentinel) {
|
|
143
|
+
return message === sentinel || message.startsWith(`${sentinel}: `);
|
|
144
|
+
}
|
|
145
|
+
function withDetail(sentinel, detail) {
|
|
146
|
+
return detail === null || detail === "" ? sentinel : `${sentinel}: ${detail}`;
|
|
147
|
+
}
|
|
148
|
+
function sentinelDetail(message, sentinel) {
|
|
149
|
+
return message.startsWith(`${sentinel}: `) ? message.slice(sentinel.length + 2) : null;
|
|
150
|
+
}
|
|
151
|
+
function splitSentinel(message) {
|
|
152
|
+
const separator = message.indexOf(": ");
|
|
153
|
+
return separator === -1 ? { sentinel: message, detail: null } : { sentinel: message.slice(0, separator), detail: message.slice(separator + 2) };
|
|
154
|
+
}
|
|
155
|
+
function writeErrorDetail(message, sentinel) {
|
|
156
|
+
const detail = sentinelDetail(message, sentinel);
|
|
157
|
+
if (detail !== null) {
|
|
158
|
+
process.stderr.write(` ${detail}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function promptYesNo(prompt) {
|
|
163
|
+
if (!process.stdin.isTTY) return true;
|
|
164
|
+
process.stderr.write(prompt);
|
|
165
|
+
return await new Promise((resolve2) => {
|
|
166
|
+
process.stdin.setEncoding("utf8");
|
|
167
|
+
let answered = false;
|
|
168
|
+
const handler = (data) => {
|
|
169
|
+
if (answered) return;
|
|
170
|
+
answered = true;
|
|
171
|
+
process.stdin.removeListener("data", handler);
|
|
172
|
+
process.stdin.pause();
|
|
173
|
+
const trimmed = data.trim().toLowerCase();
|
|
174
|
+
resolve2(trimmed === "y" || trimmed === "yes");
|
|
175
|
+
};
|
|
176
|
+
process.stdin.resume();
|
|
177
|
+
process.stdin.on("data", handler);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function parseWorkItemId(idStr) {
|
|
181
|
+
const id = Number.parseInt(idStr, 10);
|
|
182
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
185
|
+
`
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
return id;
|
|
190
|
+
}
|
|
191
|
+
function validateOrgProjectPair(options) {
|
|
192
|
+
const hasOrg = options.org !== void 0;
|
|
193
|
+
const hasProject = options.project !== void 0;
|
|
194
|
+
if (hasOrg !== hasProject) {
|
|
195
|
+
process.stderr.write(
|
|
196
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
197
|
+
);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function validateSource(options) {
|
|
202
|
+
const hasContent = options.content !== void 0;
|
|
203
|
+
const hasFile = options.file !== void 0;
|
|
204
|
+
if (hasContent === hasFile) {
|
|
205
|
+
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function formatCreateError(err) {
|
|
210
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
211
|
+
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
212
|
+
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
213
|
+
if (requiredMatches.length > 0) {
|
|
214
|
+
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
215
|
+
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
216
|
+
}
|
|
217
|
+
return `Create rejected: ${message}`;
|
|
218
|
+
}
|
|
219
|
+
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
220
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
221
|
+
const msg = error.message;
|
|
222
|
+
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
223
|
+
if (isSentinel(msg, "AUTH_FAILED")) {
|
|
224
|
+
process.stderr.write(
|
|
225
|
+
`Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
|
|
226
|
+
`
|
|
227
|
+
);
|
|
228
|
+
writeErrorDetail(msg, "AUTH_FAILED");
|
|
229
|
+
} else if (isSentinel(msg, "PERMISSION_DENIED")) {
|
|
230
|
+
process.stderr.write(
|
|
231
|
+
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
232
|
+
`
|
|
233
|
+
);
|
|
234
|
+
writeErrorDetail(msg, "PERMISSION_DENIED");
|
|
235
|
+
} else if (msg.startsWith("NOT_FOUND")) {
|
|
236
|
+
process.stderr.write(
|
|
237
|
+
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
238
|
+
`
|
|
239
|
+
);
|
|
240
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
243
|
+
);
|
|
244
|
+
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
245
|
+
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
246
|
+
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
247
|
+
`);
|
|
248
|
+
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
249
|
+
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
250
|
+
`);
|
|
251
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
252
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
253
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
254
|
+
`);
|
|
255
|
+
} else {
|
|
256
|
+
process.stderr.write(`Error: ${msg}
|
|
257
|
+
`);
|
|
258
|
+
}
|
|
259
|
+
if (exit) {
|
|
260
|
+
process.exit(1);
|
|
261
|
+
} else {
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
126
266
|
// src/services/image-download.ts
|
|
127
267
|
import { Jimp } from "jimp";
|
|
128
268
|
import { writeFile } from "fs/promises";
|
|
@@ -326,65 +466,133 @@ async function fetchRaw(url, init) {
|
|
|
326
466
|
const body = await response.text();
|
|
327
467
|
return { status: response.status, body };
|
|
328
468
|
}
|
|
329
|
-
|
|
469
|
+
var MAX_DETAIL_CHARS = 500;
|
|
470
|
+
var MAX_RAW_BODY_CHARS = 200;
|
|
471
|
+
var failureDetails = /* @__PURE__ */ new WeakMap();
|
|
472
|
+
function renderErrorFields(body, requireMessage = false) {
|
|
473
|
+
let parsed;
|
|
474
|
+
try {
|
|
475
|
+
parsed = JSON.parse(body);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
480
|
+
return renderErrorRecord(parsed, requireMessage);
|
|
481
|
+
}
|
|
482
|
+
function renderErrorRecord(record, requireMessage) {
|
|
483
|
+
const parts = [];
|
|
484
|
+
const message = typeof record.message === "string" ? record.message.trim() : "";
|
|
485
|
+
if (message !== "") {
|
|
486
|
+
parts.push(message);
|
|
487
|
+
} else if (requireMessage) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
if (typeof record.typeKey === "string" && record.typeKey.trim() !== "") {
|
|
491
|
+
parts.push(`[${record.typeKey.trim()}]`);
|
|
492
|
+
} else if (typeof record.errorCode === "number" || typeof record.errorCode === "string") {
|
|
493
|
+
parts.push(`[errorCode ${record.errorCode}]`);
|
|
494
|
+
}
|
|
495
|
+
return parts.length === 0 ? null : parts.join(" ");
|
|
496
|
+
}
|
|
497
|
+
function describeFailureBody(body, contentType) {
|
|
498
|
+
if (body === null) return null;
|
|
499
|
+
const trimmed = body.trim();
|
|
500
|
+
if (trimmed === "") return null;
|
|
501
|
+
if (contentType.toLowerCase().startsWith("text/html") || trimmed.startsWith("<")) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const redacted = redactBody(trimmed) ?? trimmed;
|
|
505
|
+
return truncateDetail(renderErrorFields(redacted) ?? redacted.slice(0, MAX_RAW_BODY_CHARS));
|
|
506
|
+
}
|
|
507
|
+
function truncateDetail(detail) {
|
|
508
|
+
const collapsed = redactText(detail).replace(/\s+/g, " ").trim();
|
|
509
|
+
if (collapsed === "") return null;
|
|
510
|
+
return collapsed.length > MAX_DETAIL_CHARS ? `${collapsed.slice(0, MAX_DETAIL_CHARS)}\u2026(truncated)` : collapsed;
|
|
511
|
+
}
|
|
512
|
+
function httpError(response) {
|
|
513
|
+
return new Error(withDetail(`HTTP_${response.status}`, failureDetails.get(response) ?? null));
|
|
514
|
+
}
|
|
515
|
+
var failureBodies = /* @__PURE__ */ new WeakMap();
|
|
516
|
+
async function readFailureBody(response, tracedBody) {
|
|
517
|
+
if (tracedBody !== null) return tracedBody;
|
|
518
|
+
try {
|
|
519
|
+
return await response.clone().text();
|
|
520
|
+
} catch {
|
|
521
|
+
try {
|
|
522
|
+
return await response.text();
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function writeTraceEntry(writer, url, init, response, responseBody) {
|
|
529
|
+
const respHeaders = {};
|
|
530
|
+
response.headers.forEach((v, k) => {
|
|
531
|
+
respHeaders[k] = v;
|
|
532
|
+
});
|
|
533
|
+
const entry = {
|
|
534
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
536
|
+
url: redactUrl(url),
|
|
537
|
+
requestHeaders: redactHeaders(init.headers ?? {}),
|
|
538
|
+
requestBody: typeof init.body === "string" ? redactBody(init.body) : null,
|
|
539
|
+
responseStatus: response.status,
|
|
540
|
+
responseHeaders: respHeaders,
|
|
541
|
+
responseBody
|
|
542
|
+
};
|
|
543
|
+
writer.append(entry);
|
|
544
|
+
}
|
|
545
|
+
async function traceResponse(url, init, response) {
|
|
330
546
|
const writer = getActiveTraceWriter();
|
|
547
|
+
if (!writer) return null;
|
|
548
|
+
let responseBody = "";
|
|
549
|
+
try {
|
|
550
|
+
responseBody = await response.clone().text();
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
writeTraceEntry(writer, url, init, response, responseBody);
|
|
554
|
+
return responseBody;
|
|
555
|
+
}
|
|
556
|
+
async function fetchWithErrors(url, init) {
|
|
331
557
|
let response;
|
|
332
558
|
try {
|
|
333
559
|
response = await fetch(url, init);
|
|
334
560
|
} catch (err) {
|
|
335
561
|
throw new Error("NETWORK_ERROR", { cause: err });
|
|
336
562
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
const respHeaders = {};
|
|
347
|
-
response.headers.forEach((v, k) => {
|
|
348
|
-
respHeaders[k] = v;
|
|
349
|
-
});
|
|
350
|
-
const entry = {
|
|
351
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
352
|
-
method: (init.method ?? "GET").toUpperCase(),
|
|
353
|
-
url: redactUrl(url),
|
|
354
|
-
requestHeaders: reqHeaders,
|
|
355
|
-
requestBody: reqBody ?? null,
|
|
356
|
-
responseStatus: response.status,
|
|
357
|
-
responseHeaders: respHeaders,
|
|
358
|
-
responseBody
|
|
359
|
-
};
|
|
360
|
-
writer.append(entry);
|
|
563
|
+
const tracedBody = await traceResponse(url, init, response);
|
|
564
|
+
const contentType = response.headers?.get("content-type") ?? "";
|
|
565
|
+
let failureDetail = null;
|
|
566
|
+
if (!response.ok) {
|
|
567
|
+
const body = await readFailureBody(response, tracedBody);
|
|
568
|
+
if (body !== null) failureBodies.set(response, body);
|
|
569
|
+
failureDetail = describeFailureBody(body, contentType);
|
|
570
|
+
if (failureDetail !== null) failureDetails.set(response, failureDetail);
|
|
361
571
|
}
|
|
362
|
-
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
363
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
572
|
+
if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
|
|
573
|
+
if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
|
|
364
574
|
if (response.status === 404) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const body = await response.text();
|
|
368
|
-
detail = ` | url=${url} | body=${body}`;
|
|
369
|
-
} catch {
|
|
370
|
-
}
|
|
371
|
-
throw new Error(`NOT_FOUND${detail}`);
|
|
575
|
+
const body = failureBodies.get(response);
|
|
576
|
+
throw new Error(body === void 0 ? "NOT_FOUND" : `NOT_FOUND | url=${url} | body=${body}`);
|
|
372
577
|
}
|
|
373
|
-
const contentType = response.headers?.get("content-type") ?? "";
|
|
374
578
|
if (contentType.toLowerCase().startsWith("text/html")) {
|
|
375
579
|
throw new Error("AUTH_FAILED");
|
|
376
580
|
}
|
|
377
581
|
return response;
|
|
378
582
|
}
|
|
379
583
|
async function readResponseMessage(response) {
|
|
584
|
+
const captured = failureBodies.get(response);
|
|
585
|
+
const rendered = captured === void 0 ? await renderErrorStream(response) : renderErrorFields(redactBody(captured.trim()) ?? captured.trim(), true);
|
|
586
|
+
return rendered === null ? null : truncateDetail(rendered);
|
|
587
|
+
}
|
|
588
|
+
async function renderErrorStream(response) {
|
|
380
589
|
try {
|
|
381
590
|
const body = await response.json();
|
|
382
|
-
if (typeof body
|
|
383
|
-
|
|
384
|
-
}
|
|
591
|
+
if (typeof body !== "object" || body === null) return null;
|
|
592
|
+
return renderErrorRecord(body, true);
|
|
385
593
|
} catch {
|
|
594
|
+
return null;
|
|
386
595
|
}
|
|
387
|
-
return null;
|
|
388
596
|
}
|
|
389
597
|
function normalizeFieldList(fields) {
|
|
390
598
|
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
@@ -464,7 +672,7 @@ async function readWriteResponse(response, errorCode) {
|
|
|
464
672
|
throw new Error(`${errorCode}: ${serverMessage}`);
|
|
465
673
|
}
|
|
466
674
|
if (!response.ok) {
|
|
467
|
-
throw
|
|
675
|
+
throw httpError(response);
|
|
468
676
|
}
|
|
469
677
|
const data = await response.json();
|
|
470
678
|
return {
|
|
@@ -487,7 +695,7 @@ async function getWorkItemFields(context, id, cred) {
|
|
|
487
695
|
}
|
|
488
696
|
}
|
|
489
697
|
if (!response.ok) {
|
|
490
|
-
throw
|
|
698
|
+
throw httpError(response);
|
|
491
699
|
}
|
|
492
700
|
const data = await response.json();
|
|
493
701
|
return data.fields;
|
|
@@ -539,7 +747,7 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
539
747
|
}
|
|
540
748
|
}
|
|
541
749
|
if (!response.ok) {
|
|
542
|
-
throw
|
|
750
|
+
throw httpError(response);
|
|
543
751
|
}
|
|
544
752
|
return await response.json();
|
|
545
753
|
}
|
|
@@ -550,7 +758,7 @@ async function getOrgFieldNames(context, cred) {
|
|
|
550
758
|
url.searchParams.set("api-version", "7.1");
|
|
551
759
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
552
760
|
if (!response.ok) {
|
|
553
|
-
throw
|
|
761
|
+
throw httpError(response);
|
|
554
762
|
}
|
|
555
763
|
const data = await response.json();
|
|
556
764
|
return (data.value ?? []).map((f) => f.referenceName);
|
|
@@ -632,7 +840,7 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
|
632
840
|
}
|
|
633
841
|
}
|
|
634
842
|
if (!response.ok) {
|
|
635
|
-
throw
|
|
843
|
+
throw httpError(response);
|
|
636
844
|
}
|
|
637
845
|
const data = await response.json();
|
|
638
846
|
const value = data.fields[fieldName];
|
|
@@ -650,7 +858,7 @@ async function listWorkItemComments(context, id, cred) {
|
|
|
650
858
|
{ headers: authHeaders(cred) }
|
|
651
859
|
);
|
|
652
860
|
if (!response.ok) {
|
|
653
|
-
throw
|
|
861
|
+
throw httpError(response);
|
|
654
862
|
}
|
|
655
863
|
const data = await response.json();
|
|
656
864
|
comments.push(
|
|
@@ -680,7 +888,7 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
|
680
888
|
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
681
889
|
}
|
|
682
890
|
if (!response.ok) {
|
|
683
|
-
throw
|
|
891
|
+
throw httpError(response);
|
|
684
892
|
}
|
|
685
893
|
const data = await response.json();
|
|
686
894
|
return {
|
|
@@ -732,7 +940,7 @@ async function applyWorkItemPatch(context, id, cred, operations) {
|
|
|
732
940
|
async function downloadAttachment(url, cred) {
|
|
733
941
|
const response = await fetchWithErrors(url, { headers: authHeaders(cred) });
|
|
734
942
|
if (!response.ok) {
|
|
735
|
-
throw
|
|
943
|
+
throw httpError(response);
|
|
736
944
|
}
|
|
737
945
|
return response.arrayBuffer();
|
|
738
946
|
}
|
|
@@ -757,7 +965,7 @@ async function createAttachment(context, fileName, content, cred) {
|
|
|
757
965
|
}
|
|
758
966
|
}
|
|
759
967
|
if (!response.ok) {
|
|
760
|
-
throw
|
|
968
|
+
throw httpError(response);
|
|
761
969
|
}
|
|
762
970
|
return await response.json();
|
|
763
971
|
}
|
|
@@ -1546,109 +1754,6 @@ function toMarkdown(content) {
|
|
|
1546
1754
|
return decodeEntitiesInMarkdownCodeSpans(decodeAdoEntitiesInMarkdown(content));
|
|
1547
1755
|
}
|
|
1548
1756
|
|
|
1549
|
-
// src/services/command-helpers.ts
|
|
1550
|
-
async function promptYesNo(prompt) {
|
|
1551
|
-
if (!process.stdin.isTTY) return true;
|
|
1552
|
-
process.stderr.write(prompt);
|
|
1553
|
-
return await new Promise((resolve2) => {
|
|
1554
|
-
process.stdin.setEncoding("utf8");
|
|
1555
|
-
let answered = false;
|
|
1556
|
-
const handler = (data) => {
|
|
1557
|
-
if (answered) return;
|
|
1558
|
-
answered = true;
|
|
1559
|
-
process.stdin.removeListener("data", handler);
|
|
1560
|
-
process.stdin.pause();
|
|
1561
|
-
const trimmed = data.trim().toLowerCase();
|
|
1562
|
-
resolve2(trimmed === "y" || trimmed === "yes");
|
|
1563
|
-
};
|
|
1564
|
-
process.stdin.resume();
|
|
1565
|
-
process.stdin.on("data", handler);
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
function parseWorkItemId(idStr) {
|
|
1569
|
-
const id = Number.parseInt(idStr, 10);
|
|
1570
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
1571
|
-
process.stderr.write(
|
|
1572
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
1573
|
-
`
|
|
1574
|
-
);
|
|
1575
|
-
process.exit(1);
|
|
1576
|
-
}
|
|
1577
|
-
return id;
|
|
1578
|
-
}
|
|
1579
|
-
function validateOrgProjectPair(options) {
|
|
1580
|
-
const hasOrg = options.org !== void 0;
|
|
1581
|
-
const hasProject = options.project !== void 0;
|
|
1582
|
-
if (hasOrg !== hasProject) {
|
|
1583
|
-
process.stderr.write(
|
|
1584
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
1585
|
-
);
|
|
1586
|
-
process.exit(1);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function validateSource(options) {
|
|
1590
|
-
const hasContent = options.content !== void 0;
|
|
1591
|
-
const hasFile = options.file !== void 0;
|
|
1592
|
-
if (hasContent === hasFile) {
|
|
1593
|
-
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
1594
|
-
process.exit(1);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function formatCreateError(err) {
|
|
1598
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1599
|
-
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
1600
|
-
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
1601
|
-
if (requiredMatches.length > 0) {
|
|
1602
|
-
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
1603
|
-
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
1604
|
-
}
|
|
1605
|
-
return `Create rejected: ${message}`;
|
|
1606
|
-
}
|
|
1607
|
-
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
1608
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1609
|
-
const msg = error.message;
|
|
1610
|
-
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
1611
|
-
if (msg === "AUTH_FAILED") {
|
|
1612
|
-
process.stderr.write(
|
|
1613
|
-
`Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
|
|
1614
|
-
`
|
|
1615
|
-
);
|
|
1616
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
1617
|
-
process.stderr.write(
|
|
1618
|
-
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
1619
|
-
`
|
|
1620
|
-
);
|
|
1621
|
-
} else if (msg.startsWith("NOT_FOUND")) {
|
|
1622
|
-
process.stderr.write(
|
|
1623
|
-
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
1624
|
-
`
|
|
1625
|
-
);
|
|
1626
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
1627
|
-
process.stderr.write(
|
|
1628
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
1629
|
-
);
|
|
1630
|
-
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
1631
|
-
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
1632
|
-
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
1633
|
-
`);
|
|
1634
|
-
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
1635
|
-
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
1636
|
-
`);
|
|
1637
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
1638
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
1639
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
1640
|
-
`);
|
|
1641
|
-
} else {
|
|
1642
|
-
process.stderr.write(`Error: ${msg}
|
|
1643
|
-
`);
|
|
1644
|
-
}
|
|
1645
|
-
if (exit) {
|
|
1646
|
-
process.exit(1);
|
|
1647
|
-
} else {
|
|
1648
|
-
process.exitCode = 1;
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
1757
|
// src/commands/get-item.ts
|
|
1653
1758
|
function parseRequestedFields(raw) {
|
|
1654
1759
|
if (raw === void 0) return void 0;
|
|
@@ -3132,10 +3237,10 @@ function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
|
|
|
3132
3237
|
};
|
|
3133
3238
|
}
|
|
3134
3239
|
function isUpdateWriteError(err) {
|
|
3135
|
-
return err.message
|
|
3240
|
+
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
3241
|
}
|
|
3137
3242
|
function isCreateWriteError(err) {
|
|
3138
|
-
return err.message
|
|
3243
|
+
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
3244
|
}
|
|
3140
3245
|
function handleUpsertError(err, id, context) {
|
|
3141
3246
|
if (!(err instanceof Error)) {
|
|
@@ -3465,7 +3570,7 @@ function isThreadResolved(status2) {
|
|
|
3465
3570
|
}
|
|
3466
3571
|
async function readJsonResponse(response) {
|
|
3467
3572
|
if (!response.ok) {
|
|
3468
|
-
throw
|
|
3573
|
+
throw httpError(response);
|
|
3469
3574
|
}
|
|
3470
3575
|
return response.json();
|
|
3471
3576
|
}
|
|
@@ -3552,19 +3657,35 @@ async function getPullRequestBuilds(context, cred, prId) {
|
|
|
3552
3657
|
isBlocking: null
|
|
3553
3658
|
}));
|
|
3554
3659
|
}
|
|
3660
|
+
var MAX_PR_DESCRIPTION_CHARS = 4e3;
|
|
3661
|
+
var DESCRIPTION_SEPARATOR = "\n\n";
|
|
3555
3662
|
function composeDescription(description, template) {
|
|
3556
|
-
if (description
|
|
3557
|
-
return
|
|
3558
|
-
|
|
3559
|
-
${template.content}`;
|
|
3560
|
-
}
|
|
3561
|
-
if (description !== void 0) {
|
|
3562
|
-
return description;
|
|
3563
|
-
}
|
|
3564
|
-
if (template !== null) {
|
|
3565
|
-
return template.content;
|
|
3663
|
+
if (description === void 0 && template === null) {
|
|
3664
|
+
return null;
|
|
3566
3665
|
}
|
|
3567
|
-
|
|
3666
|
+
const provided = description ?? "";
|
|
3667
|
+
const templateContent = template?.content ?? "";
|
|
3668
|
+
const separator = description !== void 0 && template !== null ? DESCRIPTION_SEPARATOR : "";
|
|
3669
|
+
const text = `${provided}${separator}${templateContent}`;
|
|
3670
|
+
return {
|
|
3671
|
+
text,
|
|
3672
|
+
providedChars: provided.length,
|
|
3673
|
+
separatorChars: separator.length,
|
|
3674
|
+
templateChars: templateContent.length,
|
|
3675
|
+
templatePath: template?.path ?? null,
|
|
3676
|
+
totalChars: text.length
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
function formatDescriptionOverflow(composed) {
|
|
3680
|
+
const overflow = composed.totalChars - MAX_PR_DESCRIPTION_CHARS;
|
|
3681
|
+
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.`;
|
|
3682
|
+
}
|
|
3683
|
+
function formatContributions(composed) {
|
|
3684
|
+
if (composed.templatePath === null) return "";
|
|
3685
|
+
return ` (${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} from the repository pull request template ${composed.templatePath})`;
|
|
3686
|
+
}
|
|
3687
|
+
function describeDescriptionBudget(composed) {
|
|
3688
|
+
return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
|
|
3568
3689
|
}
|
|
3569
3690
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
3570
3691
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
@@ -3585,29 +3706,40 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3585
3706
|
const repository = await getRepository(context, repo, cred);
|
|
3586
3707
|
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
3587
3708
|
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3709
|
+
const composed = composeDescription(description, template);
|
|
3710
|
+
if (composed === null) {
|
|
3590
3711
|
throw new Error("DESCRIPTION_REQUIRED");
|
|
3591
3712
|
}
|
|
3713
|
+
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
3714
|
+
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
3715
|
+
}
|
|
3592
3716
|
const payload = {
|
|
3593
3717
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
3594
3718
|
targetRefName: "refs/heads/develop",
|
|
3595
3719
|
title,
|
|
3596
|
-
description:
|
|
3720
|
+
description: composed.text
|
|
3597
3721
|
};
|
|
3598
3722
|
const url = new URL(
|
|
3599
3723
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
3600
3724
|
);
|
|
3601
3725
|
url.searchParams.set("api-version", "7.1");
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3726
|
+
let data;
|
|
3727
|
+
try {
|
|
3728
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3729
|
+
method: "POST",
|
|
3730
|
+
headers: {
|
|
3731
|
+
...authHeaders(cred),
|
|
3732
|
+
"Content-Type": "application/json"
|
|
3733
|
+
},
|
|
3734
|
+
body: JSON.stringify(payload)
|
|
3735
|
+
});
|
|
3736
|
+
data = await readJsonResponse(response);
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
if (err instanceof Error && err.message.startsWith("HTTP_400")) {
|
|
3739
|
+
throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
|
|
3740
|
+
}
|
|
3741
|
+
throw err;
|
|
3742
|
+
}
|
|
3611
3743
|
return {
|
|
3612
3744
|
branch: sourceBranch,
|
|
3613
3745
|
targetBranch: "develop",
|
|
@@ -3745,7 +3877,7 @@ async function patchWorkItemRelations(context, cred, workItemId, operation) {
|
|
|
3745
3877
|
body: JSON.stringify([operation])
|
|
3746
3878
|
});
|
|
3747
3879
|
if (!response.ok) {
|
|
3748
|
-
throw
|
|
3880
|
+
throw httpError(response);
|
|
3749
3881
|
}
|
|
3750
3882
|
}
|
|
3751
3883
|
async function linkWorkItemToPullRequest(context, repo, cred, prId, workItemId) {
|
|
@@ -3794,8 +3926,8 @@ async function resolveReviewerIdentity(org, cred, input) {
|
|
|
3794
3926
|
headers: authHeaders(cred)
|
|
3795
3927
|
});
|
|
3796
3928
|
} catch (err) {
|
|
3797
|
-
if (err instanceof Error && err.message
|
|
3798
|
-
throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
|
|
3929
|
+
if (err instanceof Error && isSentinel(err.message, "AUTH_FAILED")) {
|
|
3930
|
+
throw new Error(withDetail("IDENTITY_SCOPE_MISSING", sentinelDetail(err.message, "AUTH_FAILED")), { cause: err });
|
|
3799
3931
|
}
|
|
3800
3932
|
throw err;
|
|
3801
3933
|
}
|
|
@@ -3853,7 +3985,7 @@ async function removePullRequestReviewer(context, repo, cred, prId, reviewerId)
|
|
|
3853
3985
|
headers: authHeaders(cred)
|
|
3854
3986
|
});
|
|
3855
3987
|
if (!response.ok) {
|
|
3856
|
-
throw
|
|
3988
|
+
throw httpError(response);
|
|
3857
3989
|
}
|
|
3858
3990
|
return { reviewer: existing, noop: false };
|
|
3859
3991
|
}
|
|
@@ -3892,7 +4024,7 @@ async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
|
|
|
3892
4024
|
throw err;
|
|
3893
4025
|
}
|
|
3894
4026
|
if (!response.ok) {
|
|
3895
|
-
throw
|
|
4027
|
+
throw httpError(response);
|
|
3896
4028
|
}
|
|
3897
4029
|
return response.text();
|
|
3898
4030
|
}
|
|
@@ -4027,7 +4159,7 @@ function writeError(message, exitCode = 1) {
|
|
|
4027
4159
|
}
|
|
4028
4160
|
function handlePrCommandError(err, context, mode = "read") {
|
|
4029
4161
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4030
|
-
if (error.message
|
|
4162
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
4031
4163
|
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
4032
4164
|
writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`, EXIT_NOT_PERMITTED);
|
|
4033
4165
|
const credentialHint = describeResolvedCredential();
|
|
@@ -4035,17 +4167,20 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4035
4167
|
process.stderr.write(` ${credentialHint}
|
|
4036
4168
|
`);
|
|
4037
4169
|
}
|
|
4170
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
4038
4171
|
return;
|
|
4039
4172
|
}
|
|
4040
|
-
if (error.message
|
|
4173
|
+
if (isSentinel(error.message, "IDENTITY_SCOPE_MISSING")) {
|
|
4041
4174
|
writeError(
|
|
4042
4175
|
'Could not resolve reviewer identity: your PAT is missing the "Identity (Read)" scope required by the Azure DevOps identities API (separate from Code scope).',
|
|
4043
4176
|
EXIT_NOT_PERMITTED
|
|
4044
4177
|
);
|
|
4178
|
+
writeErrorDetail(error.message, "IDENTITY_SCOPE_MISSING");
|
|
4045
4179
|
return;
|
|
4046
4180
|
}
|
|
4047
|
-
if (error.message
|
|
4181
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
4048
4182
|
writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
|
|
4183
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
4049
4184
|
return;
|
|
4050
4185
|
}
|
|
4051
4186
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -4057,7 +4192,12 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4057
4192
|
return;
|
|
4058
4193
|
}
|
|
4059
4194
|
if (error.message.startsWith("HTTP_")) {
|
|
4060
|
-
|
|
4195
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
4196
|
+
writeError(`Azure DevOps request failed with ${sentinel}.`);
|
|
4197
|
+
if (detail !== null) {
|
|
4198
|
+
process.stderr.write(` ${detail}
|
|
4199
|
+
`);
|
|
4200
|
+
}
|
|
4061
4201
|
return;
|
|
4062
4202
|
}
|
|
4063
4203
|
writeError(error.message);
|
|
@@ -4232,6 +4372,23 @@ function createPrStatusCommand() {
|
|
|
4232
4372
|
});
|
|
4233
4373
|
return command;
|
|
4234
4374
|
}
|
|
4375
|
+
function handlePrOpenError(err, context) {
|
|
4376
|
+
const message = err instanceof Error ? err.message : "";
|
|
4377
|
+
if (message.startsWith("AMBIGUOUS_PRS:")) {
|
|
4378
|
+
const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4379
|
+
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
if (message === "DESCRIPTION_REQUIRED") {
|
|
4383
|
+
writeError("--description is required for pull request creation.");
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
4387
|
+
writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
handlePrCommandError(err, context, "write");
|
|
4391
|
+
}
|
|
4235
4392
|
function createPrOpenCommand() {
|
|
4236
4393
|
const command = new Command12("open");
|
|
4237
4394
|
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
@@ -4280,16 +4437,7 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
4280
4437
|
`
|
|
4281
4438
|
);
|
|
4282
4439
|
} catch (err) {
|
|
4283
|
-
|
|
4284
|
-
const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4285
|
-
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
4286
|
-
return;
|
|
4287
|
-
}
|
|
4288
|
-
if (err instanceof Error && err.message === "DESCRIPTION_REQUIRED") {
|
|
4289
|
-
writeError("--description is required for pull request creation.");
|
|
4290
|
-
return;
|
|
4291
|
-
}
|
|
4292
|
-
handlePrCommandError(err, context, "write");
|
|
4440
|
+
handlePrOpenError(err, context);
|
|
4293
4441
|
}
|
|
4294
4442
|
});
|
|
4295
4443
|
return command;
|
|
@@ -5098,7 +5246,7 @@ function withApiVersion(url) {
|
|
|
5098
5246
|
}
|
|
5099
5247
|
async function readJsonResponse2(response) {
|
|
5100
5248
|
if (!response.ok) {
|
|
5101
|
-
throw
|
|
5249
|
+
throw httpError(response);
|
|
5102
5250
|
}
|
|
5103
5251
|
return await response.json();
|
|
5104
5252
|
}
|
|
@@ -5374,7 +5522,7 @@ async function getRunLog(context, cred, buildId, logId) {
|
|
|
5374
5522
|
);
|
|
5375
5523
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
5376
5524
|
if (!response.ok) {
|
|
5377
|
-
throw
|
|
5525
|
+
throw httpError(response);
|
|
5378
5526
|
}
|
|
5379
5527
|
return response.text();
|
|
5380
5528
|
}
|
|
@@ -5390,12 +5538,14 @@ function writeError2(message) {
|
|
|
5390
5538
|
}
|
|
5391
5539
|
function handlePipelineError(err, context) {
|
|
5392
5540
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
5393
|
-
if (error.message
|
|
5541
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
5394
5542
|
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
5543
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
5395
5544
|
return;
|
|
5396
5545
|
}
|
|
5397
|
-
if (error.message
|
|
5546
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
5398
5547
|
writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
|
|
5548
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
5399
5549
|
return;
|
|
5400
5550
|
}
|
|
5401
5551
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -5407,7 +5557,12 @@ function handlePipelineError(err, context) {
|
|
|
5407
5557
|
return;
|
|
5408
5558
|
}
|
|
5409
5559
|
if (error.message.startsWith("HTTP_")) {
|
|
5410
|
-
|
|
5560
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
5561
|
+
writeError2(`Azure DevOps request failed with ${sentinel}.`);
|
|
5562
|
+
if (detail !== null) {
|
|
5563
|
+
process.stderr.write(` ${detail}
|
|
5564
|
+
`);
|
|
5565
|
+
}
|
|
5411
5566
|
return;
|
|
5412
5567
|
}
|
|
5413
5568
|
writeError2(error.message);
|
|
@@ -6299,7 +6454,7 @@ function mapRelationType(raw) {
|
|
|
6299
6454
|
};
|
|
6300
6455
|
}
|
|
6301
6456
|
async function readJsonResponse3(response) {
|
|
6302
|
-
if (!response.ok) throw
|
|
6457
|
+
if (!response.ok) throw httpError(response);
|
|
6303
6458
|
return await response.json();
|
|
6304
6459
|
}
|
|
6305
6460
|
function parseTargetId(url) {
|
|
@@ -6347,7 +6502,7 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6347
6502
|
{ op: "add", path: "/relations/-", value: { rel: relType.referenceName, url: targetUrl } }
|
|
6348
6503
|
])
|
|
6349
6504
|
});
|
|
6350
|
-
if (!response.ok) throw
|
|
6505
|
+
if (!response.ok) throw httpError(response);
|
|
6351
6506
|
return { status: "added", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6352
6507
|
}
|
|
6353
6508
|
async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
@@ -6367,7 +6522,7 @@ async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6367
6522
|
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
6368
6523
|
body: JSON.stringify([{ op: "remove", path: `/relations/${index}` }])
|
|
6369
6524
|
});
|
|
6370
|
-
if (!response.ok) throw
|
|
6525
|
+
if (!response.ok) throw httpError(response);
|
|
6371
6526
|
return { status: "removed", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6372
6527
|
}
|
|
6373
6528
|
async function listWorkItemRelations(context, cred, id) {
|
|
@@ -6455,11 +6610,12 @@ function handleRelationError(err, id1) {
|
|
|
6455
6610
|
const target = id1 !== void 0 ? id1 : "unknown";
|
|
6456
6611
|
process.stderr.write(`Error: work item #${target} not found.
|
|
6457
6612
|
`);
|
|
6458
|
-
} else if (msg
|
|
6613
|
+
} else if (isSentinel(msg, "AUTH_FAILED")) {
|
|
6459
6614
|
process.stderr.write(
|
|
6460
6615
|
`Error: authentication failed. Check your PAT has Work Items \u2192 Read & Write scope.
|
|
6461
6616
|
`
|
|
6462
6617
|
);
|
|
6618
|
+
writeErrorDetail(msg, "AUTH_FAILED");
|
|
6463
6619
|
} else {
|
|
6464
6620
|
process.stderr.write(`Error: ${msg}
|
|
6465
6621
|
`);
|