azdo-cli 0.19.0-develop.690 → 0.19.0-develop.703
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 +364 -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(String.raw`(\b(?:access|refresh|api)[_-]?(?:token|key)\b${SECRET_ASSIGNMENT}`, "gi"), `$1${REDACTED}`],
|
|
85
|
+
[new RegExp(String.raw`(\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,136 @@ 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
|
+
const fields = renderErrorFields(redacted);
|
|
506
|
+
if (fields !== null) return truncateDetail(fields);
|
|
507
|
+
return truncateDetail(redactText(redacted).slice(0, MAX_RAW_BODY_CHARS));
|
|
508
|
+
}
|
|
509
|
+
function truncateDetail(detail) {
|
|
510
|
+
const collapsed = redactText(detail).replace(/\s+/g, " ").trim();
|
|
511
|
+
if (collapsed === "") return null;
|
|
512
|
+
return collapsed.length > MAX_DETAIL_CHARS ? `${collapsed.slice(0, MAX_DETAIL_CHARS)}\u2026(truncated)` : collapsed;
|
|
513
|
+
}
|
|
514
|
+
function httpError(response) {
|
|
515
|
+
return new Error(withDetail(`HTTP_${response.status}`, failureDetails.get(response) ?? null));
|
|
516
|
+
}
|
|
517
|
+
var failureBodies = /* @__PURE__ */ new WeakMap();
|
|
518
|
+
async function readFailureBody(response, tracedBody) {
|
|
519
|
+
if (tracedBody !== null) return tracedBody;
|
|
520
|
+
try {
|
|
521
|
+
return await response.clone().text();
|
|
522
|
+
} catch {
|
|
523
|
+
try {
|
|
524
|
+
return await response.text();
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function writeTraceEntry(writer, url, init, response, responseBody) {
|
|
531
|
+
const respHeaders = {};
|
|
532
|
+
response.headers.forEach((v, k) => {
|
|
533
|
+
respHeaders[k] = v;
|
|
534
|
+
});
|
|
535
|
+
const entry = {
|
|
536
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
537
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
538
|
+
url: redactUrl(url),
|
|
539
|
+
requestHeaders: redactHeaders(init.headers ?? {}),
|
|
540
|
+
requestBody: typeof init.body === "string" ? redactBody(init.body) : null,
|
|
541
|
+
responseStatus: response.status,
|
|
542
|
+
responseHeaders: respHeaders,
|
|
543
|
+
responseBody
|
|
544
|
+
};
|
|
545
|
+
writer.append(entry);
|
|
546
|
+
}
|
|
547
|
+
async function traceResponse(url, init, response) {
|
|
330
548
|
const writer = getActiveTraceWriter();
|
|
549
|
+
if (!writer) return null;
|
|
550
|
+
let responseBody = null;
|
|
551
|
+
try {
|
|
552
|
+
responseBody = await response.clone().text();
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
writeTraceEntry(writer, url, init, response, responseBody ?? "");
|
|
556
|
+
return responseBody;
|
|
557
|
+
}
|
|
558
|
+
async function fetchWithErrors(url, init) {
|
|
331
559
|
let response;
|
|
332
560
|
try {
|
|
333
561
|
response = await fetch(url, init);
|
|
334
562
|
} catch (err) {
|
|
335
563
|
throw new Error("NETWORK_ERROR", { cause: err });
|
|
336
564
|
}
|
|
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);
|
|
565
|
+
const tracedBody = await traceResponse(url, init, response);
|
|
566
|
+
const contentType = response.headers?.get("content-type") ?? "";
|
|
567
|
+
let failureDetail = null;
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
const body = await readFailureBody(response, tracedBody);
|
|
570
|
+
if (body !== null) failureBodies.set(response, body);
|
|
571
|
+
failureDetail = describeFailureBody(body, contentType);
|
|
572
|
+
if (failureDetail !== null) failureDetails.set(response, failureDetail);
|
|
361
573
|
}
|
|
362
|
-
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
363
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
574
|
+
if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
|
|
575
|
+
if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
|
|
364
576
|
if (response.status === 404) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
detail = ` | url=${url} | body=${body}`;
|
|
369
|
-
} catch {
|
|
370
|
-
}
|
|
371
|
-
throw new Error(`NOT_FOUND${detail}`);
|
|
577
|
+
throw new Error(
|
|
578
|
+
failureDetail === null ? "NOT_FOUND" : `NOT_FOUND | url=${redactUrl(url)} | body=${failureDetail}`
|
|
579
|
+
);
|
|
372
580
|
}
|
|
373
|
-
const contentType = response.headers?.get("content-type") ?? "";
|
|
374
581
|
if (contentType.toLowerCase().startsWith("text/html")) {
|
|
375
582
|
throw new Error("AUTH_FAILED");
|
|
376
583
|
}
|
|
377
584
|
return response;
|
|
378
585
|
}
|
|
379
586
|
async function readResponseMessage(response) {
|
|
587
|
+
const captured = failureBodies.get(response);
|
|
588
|
+
const rendered = captured === void 0 ? await renderErrorStream(response) : renderErrorFields(redactBody(captured.trim()) ?? captured.trim(), true);
|
|
589
|
+
return rendered === null ? null : truncateDetail(rendered);
|
|
590
|
+
}
|
|
591
|
+
async function renderErrorStream(response) {
|
|
380
592
|
try {
|
|
381
593
|
const body = await response.json();
|
|
382
|
-
if (typeof body
|
|
383
|
-
|
|
384
|
-
}
|
|
594
|
+
if (typeof body !== "object" || body === null) return null;
|
|
595
|
+
return renderErrorRecord(body, true);
|
|
385
596
|
} catch {
|
|
597
|
+
return null;
|
|
386
598
|
}
|
|
387
|
-
return null;
|
|
388
599
|
}
|
|
389
600
|
function normalizeFieldList(fields) {
|
|
390
601
|
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
@@ -464,7 +675,7 @@ async function readWriteResponse(response, errorCode) {
|
|
|
464
675
|
throw new Error(`${errorCode}: ${serverMessage}`);
|
|
465
676
|
}
|
|
466
677
|
if (!response.ok) {
|
|
467
|
-
throw
|
|
678
|
+
throw httpError(response);
|
|
468
679
|
}
|
|
469
680
|
const data = await response.json();
|
|
470
681
|
return {
|
|
@@ -487,7 +698,7 @@ async function getWorkItemFields(context, id, cred) {
|
|
|
487
698
|
}
|
|
488
699
|
}
|
|
489
700
|
if (!response.ok) {
|
|
490
|
-
throw
|
|
701
|
+
throw httpError(response);
|
|
491
702
|
}
|
|
492
703
|
const data = await response.json();
|
|
493
704
|
return data.fields;
|
|
@@ -539,7 +750,7 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
539
750
|
}
|
|
540
751
|
}
|
|
541
752
|
if (!response.ok) {
|
|
542
|
-
throw
|
|
753
|
+
throw httpError(response);
|
|
543
754
|
}
|
|
544
755
|
return await response.json();
|
|
545
756
|
}
|
|
@@ -550,7 +761,7 @@ async function getOrgFieldNames(context, cred) {
|
|
|
550
761
|
url.searchParams.set("api-version", "7.1");
|
|
551
762
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
552
763
|
if (!response.ok) {
|
|
553
|
-
throw
|
|
764
|
+
throw httpError(response);
|
|
554
765
|
}
|
|
555
766
|
const data = await response.json();
|
|
556
767
|
return (data.value ?? []).map((f) => f.referenceName);
|
|
@@ -632,7 +843,7 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
|
632
843
|
}
|
|
633
844
|
}
|
|
634
845
|
if (!response.ok) {
|
|
635
|
-
throw
|
|
846
|
+
throw httpError(response);
|
|
636
847
|
}
|
|
637
848
|
const data = await response.json();
|
|
638
849
|
const value = data.fields[fieldName];
|
|
@@ -650,7 +861,7 @@ async function listWorkItemComments(context, id, cred) {
|
|
|
650
861
|
{ headers: authHeaders(cred) }
|
|
651
862
|
);
|
|
652
863
|
if (!response.ok) {
|
|
653
|
-
throw
|
|
864
|
+
throw httpError(response);
|
|
654
865
|
}
|
|
655
866
|
const data = await response.json();
|
|
656
867
|
comments.push(
|
|
@@ -680,7 +891,7 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
|
680
891
|
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
681
892
|
}
|
|
682
893
|
if (!response.ok) {
|
|
683
|
-
throw
|
|
894
|
+
throw httpError(response);
|
|
684
895
|
}
|
|
685
896
|
const data = await response.json();
|
|
686
897
|
return {
|
|
@@ -732,7 +943,7 @@ async function applyWorkItemPatch(context, id, cred, operations) {
|
|
|
732
943
|
async function downloadAttachment(url, cred) {
|
|
733
944
|
const response = await fetchWithErrors(url, { headers: authHeaders(cred) });
|
|
734
945
|
if (!response.ok) {
|
|
735
|
-
throw
|
|
946
|
+
throw httpError(response);
|
|
736
947
|
}
|
|
737
948
|
return response.arrayBuffer();
|
|
738
949
|
}
|
|
@@ -757,7 +968,7 @@ async function createAttachment(context, fileName, content, cred) {
|
|
|
757
968
|
}
|
|
758
969
|
}
|
|
759
970
|
if (!response.ok) {
|
|
760
|
-
throw
|
|
971
|
+
throw httpError(response);
|
|
761
972
|
}
|
|
762
973
|
return await response.json();
|
|
763
974
|
}
|
|
@@ -1546,109 +1757,6 @@ function toMarkdown(content) {
|
|
|
1546
1757
|
return decodeEntitiesInMarkdownCodeSpans(decodeAdoEntitiesInMarkdown(content));
|
|
1547
1758
|
}
|
|
1548
1759
|
|
|
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
1760
|
// src/commands/get-item.ts
|
|
1653
1761
|
function parseRequestedFields(raw) {
|
|
1654
1762
|
if (raw === void 0) return void 0;
|
|
@@ -3132,10 +3240,10 @@ function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
|
|
|
3132
3240
|
};
|
|
3133
3241
|
}
|
|
3134
3242
|
function isUpdateWriteError(err) {
|
|
3135
|
-
return err.message
|
|
3243
|
+
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
3244
|
}
|
|
3137
3245
|
function isCreateWriteError(err) {
|
|
3138
|
-
return err.message
|
|
3246
|
+
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
3247
|
}
|
|
3140
3248
|
function handleUpsertError(err, id, context) {
|
|
3141
3249
|
if (!(err instanceof Error)) {
|
|
@@ -3465,7 +3573,7 @@ function isThreadResolved(status2) {
|
|
|
3465
3573
|
}
|
|
3466
3574
|
async function readJsonResponse(response) {
|
|
3467
3575
|
if (!response.ok) {
|
|
3468
|
-
throw
|
|
3576
|
+
throw httpError(response);
|
|
3469
3577
|
}
|
|
3470
3578
|
return response.json();
|
|
3471
3579
|
}
|
|
@@ -3552,19 +3660,35 @@ async function getPullRequestBuilds(context, cred, prId) {
|
|
|
3552
3660
|
isBlocking: null
|
|
3553
3661
|
}));
|
|
3554
3662
|
}
|
|
3663
|
+
var MAX_PR_DESCRIPTION_CHARS = 4e3;
|
|
3664
|
+
var DESCRIPTION_SEPARATOR = "\n\n";
|
|
3555
3665
|
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;
|
|
3666
|
+
if (description === void 0 && template === null) {
|
|
3667
|
+
return null;
|
|
3566
3668
|
}
|
|
3567
|
-
|
|
3669
|
+
const provided = description ?? "";
|
|
3670
|
+
const templateContent = template?.content ?? "";
|
|
3671
|
+
const separator = description !== void 0 && template !== null ? DESCRIPTION_SEPARATOR : "";
|
|
3672
|
+
const text = `${provided}${separator}${templateContent}`;
|
|
3673
|
+
return {
|
|
3674
|
+
text,
|
|
3675
|
+
providedChars: provided.length,
|
|
3676
|
+
separatorChars: separator.length,
|
|
3677
|
+
templateChars: templateContent.length,
|
|
3678
|
+
templatePath: template?.path ?? null,
|
|
3679
|
+
totalChars: text.length
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
function formatDescriptionOverflow(composed) {
|
|
3683
|
+
const overflow = composed.totalChars - MAX_PR_DESCRIPTION_CHARS;
|
|
3684
|
+
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.`;
|
|
3685
|
+
}
|
|
3686
|
+
function formatContributions(composed) {
|
|
3687
|
+
if (composed.templatePath === null) return "";
|
|
3688
|
+
return ` (${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} from the repository pull request template ${composed.templatePath})`;
|
|
3689
|
+
}
|
|
3690
|
+
function describeDescriptionBudget(composed) {
|
|
3691
|
+
return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
|
|
3568
3692
|
}
|
|
3569
3693
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
3570
3694
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
@@ -3585,29 +3709,40 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3585
3709
|
const repository = await getRepository(context, repo, cred);
|
|
3586
3710
|
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
3587
3711
|
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3712
|
+
const composed = composeDescription(description, template);
|
|
3713
|
+
if (composed === null) {
|
|
3590
3714
|
throw new Error("DESCRIPTION_REQUIRED");
|
|
3591
3715
|
}
|
|
3716
|
+
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
3717
|
+
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
3718
|
+
}
|
|
3592
3719
|
const payload = {
|
|
3593
3720
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
3594
3721
|
targetRefName: "refs/heads/develop",
|
|
3595
3722
|
title,
|
|
3596
|
-
description:
|
|
3723
|
+
description: composed.text
|
|
3597
3724
|
};
|
|
3598
3725
|
const url = new URL(
|
|
3599
3726
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
3600
3727
|
);
|
|
3601
3728
|
url.searchParams.set("api-version", "7.1");
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3729
|
+
let data;
|
|
3730
|
+
try {
|
|
3731
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3732
|
+
method: "POST",
|
|
3733
|
+
headers: {
|
|
3734
|
+
...authHeaders(cred),
|
|
3735
|
+
"Content-Type": "application/json"
|
|
3736
|
+
},
|
|
3737
|
+
body: JSON.stringify(payload)
|
|
3738
|
+
});
|
|
3739
|
+
data = await readJsonResponse(response);
|
|
3740
|
+
} catch (err) {
|
|
3741
|
+
if (err instanceof Error && err.message.startsWith("HTTP_400")) {
|
|
3742
|
+
throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
|
|
3743
|
+
}
|
|
3744
|
+
throw err;
|
|
3745
|
+
}
|
|
3611
3746
|
return {
|
|
3612
3747
|
branch: sourceBranch,
|
|
3613
3748
|
targetBranch: "develop",
|
|
@@ -3745,7 +3880,7 @@ async function patchWorkItemRelations(context, cred, workItemId, operation) {
|
|
|
3745
3880
|
body: JSON.stringify([operation])
|
|
3746
3881
|
});
|
|
3747
3882
|
if (!response.ok) {
|
|
3748
|
-
throw
|
|
3883
|
+
throw httpError(response);
|
|
3749
3884
|
}
|
|
3750
3885
|
}
|
|
3751
3886
|
async function linkWorkItemToPullRequest(context, repo, cred, prId, workItemId) {
|
|
@@ -3794,8 +3929,8 @@ async function resolveReviewerIdentity(org, cred, input) {
|
|
|
3794
3929
|
headers: authHeaders(cred)
|
|
3795
3930
|
});
|
|
3796
3931
|
} catch (err) {
|
|
3797
|
-
if (err instanceof Error && err.message
|
|
3798
|
-
throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
|
|
3932
|
+
if (err instanceof Error && isSentinel(err.message, "AUTH_FAILED")) {
|
|
3933
|
+
throw new Error(withDetail("IDENTITY_SCOPE_MISSING", sentinelDetail(err.message, "AUTH_FAILED")), { cause: err });
|
|
3799
3934
|
}
|
|
3800
3935
|
throw err;
|
|
3801
3936
|
}
|
|
@@ -3853,7 +3988,7 @@ async function removePullRequestReviewer(context, repo, cred, prId, reviewerId)
|
|
|
3853
3988
|
headers: authHeaders(cred)
|
|
3854
3989
|
});
|
|
3855
3990
|
if (!response.ok) {
|
|
3856
|
-
throw
|
|
3991
|
+
throw httpError(response);
|
|
3857
3992
|
}
|
|
3858
3993
|
return { reviewer: existing, noop: false };
|
|
3859
3994
|
}
|
|
@@ -3892,7 +4027,7 @@ async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
|
|
|
3892
4027
|
throw err;
|
|
3893
4028
|
}
|
|
3894
4029
|
if (!response.ok) {
|
|
3895
|
-
throw
|
|
4030
|
+
throw httpError(response);
|
|
3896
4031
|
}
|
|
3897
4032
|
return response.text();
|
|
3898
4033
|
}
|
|
@@ -4027,7 +4162,7 @@ function writeError(message, exitCode = 1) {
|
|
|
4027
4162
|
}
|
|
4028
4163
|
function handlePrCommandError(err, context, mode = "read") {
|
|
4029
4164
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4030
|
-
if (error.message
|
|
4165
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
4031
4166
|
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
4032
4167
|
writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`, EXIT_NOT_PERMITTED);
|
|
4033
4168
|
const credentialHint = describeResolvedCredential();
|
|
@@ -4035,17 +4170,20 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4035
4170
|
process.stderr.write(` ${credentialHint}
|
|
4036
4171
|
`);
|
|
4037
4172
|
}
|
|
4173
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
4038
4174
|
return;
|
|
4039
4175
|
}
|
|
4040
|
-
if (error.message
|
|
4176
|
+
if (isSentinel(error.message, "IDENTITY_SCOPE_MISSING")) {
|
|
4041
4177
|
writeError(
|
|
4042
4178
|
'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
4179
|
EXIT_NOT_PERMITTED
|
|
4044
4180
|
);
|
|
4181
|
+
writeErrorDetail(error.message, "IDENTITY_SCOPE_MISSING");
|
|
4045
4182
|
return;
|
|
4046
4183
|
}
|
|
4047
|
-
if (error.message
|
|
4184
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
4048
4185
|
writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
|
|
4186
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
4049
4187
|
return;
|
|
4050
4188
|
}
|
|
4051
4189
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -4057,7 +4195,12 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4057
4195
|
return;
|
|
4058
4196
|
}
|
|
4059
4197
|
if (error.message.startsWith("HTTP_")) {
|
|
4060
|
-
|
|
4198
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
4199
|
+
writeError(`Azure DevOps request failed with ${sentinel}.`);
|
|
4200
|
+
if (detail !== null) {
|
|
4201
|
+
process.stderr.write(` ${detail}
|
|
4202
|
+
`);
|
|
4203
|
+
}
|
|
4061
4204
|
return;
|
|
4062
4205
|
}
|
|
4063
4206
|
writeError(error.message);
|
|
@@ -4232,6 +4375,23 @@ function createPrStatusCommand() {
|
|
|
4232
4375
|
});
|
|
4233
4376
|
return command;
|
|
4234
4377
|
}
|
|
4378
|
+
function handlePrOpenError(err, context) {
|
|
4379
|
+
const message = err instanceof Error ? err.message : "";
|
|
4380
|
+
if (message.startsWith("AMBIGUOUS_PRS:")) {
|
|
4381
|
+
const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4382
|
+
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
if (message === "DESCRIPTION_REQUIRED") {
|
|
4386
|
+
writeError("--description is required for pull request creation.");
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
4390
|
+
writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
handlePrCommandError(err, context, "write");
|
|
4394
|
+
}
|
|
4235
4395
|
function createPrOpenCommand() {
|
|
4236
4396
|
const command = new Command12("open");
|
|
4237
4397
|
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
@@ -4280,16 +4440,7 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
4280
4440
|
`
|
|
4281
4441
|
);
|
|
4282
4442
|
} 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");
|
|
4443
|
+
handlePrOpenError(err, context);
|
|
4293
4444
|
}
|
|
4294
4445
|
});
|
|
4295
4446
|
return command;
|
|
@@ -5098,7 +5249,7 @@ function withApiVersion(url) {
|
|
|
5098
5249
|
}
|
|
5099
5250
|
async function readJsonResponse2(response) {
|
|
5100
5251
|
if (!response.ok) {
|
|
5101
|
-
throw
|
|
5252
|
+
throw httpError(response);
|
|
5102
5253
|
}
|
|
5103
5254
|
return await response.json();
|
|
5104
5255
|
}
|
|
@@ -5374,7 +5525,7 @@ async function getRunLog(context, cred, buildId, logId) {
|
|
|
5374
5525
|
);
|
|
5375
5526
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
5376
5527
|
if (!response.ok) {
|
|
5377
|
-
throw
|
|
5528
|
+
throw httpError(response);
|
|
5378
5529
|
}
|
|
5379
5530
|
return response.text();
|
|
5380
5531
|
}
|
|
@@ -5390,12 +5541,14 @@ function writeError2(message) {
|
|
|
5390
5541
|
}
|
|
5391
5542
|
function handlePipelineError(err, context) {
|
|
5392
5543
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
5393
|
-
if (error.message
|
|
5544
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
5394
5545
|
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
5546
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
5395
5547
|
return;
|
|
5396
5548
|
}
|
|
5397
|
-
if (error.message
|
|
5549
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
5398
5550
|
writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
|
|
5551
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
5399
5552
|
return;
|
|
5400
5553
|
}
|
|
5401
5554
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -5407,7 +5560,12 @@ function handlePipelineError(err, context) {
|
|
|
5407
5560
|
return;
|
|
5408
5561
|
}
|
|
5409
5562
|
if (error.message.startsWith("HTTP_")) {
|
|
5410
|
-
|
|
5563
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
5564
|
+
writeError2(`Azure DevOps request failed with ${sentinel}.`);
|
|
5565
|
+
if (detail !== null) {
|
|
5566
|
+
process.stderr.write(` ${detail}
|
|
5567
|
+
`);
|
|
5568
|
+
}
|
|
5411
5569
|
return;
|
|
5412
5570
|
}
|
|
5413
5571
|
writeError2(error.message);
|
|
@@ -6299,7 +6457,7 @@ function mapRelationType(raw) {
|
|
|
6299
6457
|
};
|
|
6300
6458
|
}
|
|
6301
6459
|
async function readJsonResponse3(response) {
|
|
6302
|
-
if (!response.ok) throw
|
|
6460
|
+
if (!response.ok) throw httpError(response);
|
|
6303
6461
|
return await response.json();
|
|
6304
6462
|
}
|
|
6305
6463
|
function parseTargetId(url) {
|
|
@@ -6347,7 +6505,7 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6347
6505
|
{ op: "add", path: "/relations/-", value: { rel: relType.referenceName, url: targetUrl } }
|
|
6348
6506
|
])
|
|
6349
6507
|
});
|
|
6350
|
-
if (!response.ok) throw
|
|
6508
|
+
if (!response.ok) throw httpError(response);
|
|
6351
6509
|
return { status: "added", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6352
6510
|
}
|
|
6353
6511
|
async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
@@ -6367,7 +6525,7 @@ async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6367
6525
|
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
6368
6526
|
body: JSON.stringify([{ op: "remove", path: `/relations/${index}` }])
|
|
6369
6527
|
});
|
|
6370
|
-
if (!response.ok) throw
|
|
6528
|
+
if (!response.ok) throw httpError(response);
|
|
6371
6529
|
return { status: "removed", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6372
6530
|
}
|
|
6373
6531
|
async function listWorkItemRelations(context, cred, id) {
|
|
@@ -6455,11 +6613,12 @@ function handleRelationError(err, id1) {
|
|
|
6455
6613
|
const target = id1 !== void 0 ? id1 : "unknown";
|
|
6456
6614
|
process.stderr.write(`Error: work item #${target} not found.
|
|
6457
6615
|
`);
|
|
6458
|
-
} else if (msg
|
|
6616
|
+
} else if (isSentinel(msg, "AUTH_FAILED")) {
|
|
6459
6617
|
process.stderr.write(
|
|
6460
6618
|
`Error: authentication failed. Check your PAT has Work Items \u2192 Read & Write scope.
|
|
6461
6619
|
`
|
|
6462
6620
|
);
|
|
6621
|
+
writeErrorDetail(msg, "AUTH_FAILED");
|
|
6463
6622
|
} else {
|
|
6464
6623
|
process.stderr.write(`Error: ${msg}
|
|
6465
6624
|
`);
|