azdo-cli 0.10.1 → 0.11.0
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 +5 -2
- package/dist/chunk-C7RAZJHV.js +1087 -0
- package/dist/index.js +658 -513
- package/dist/oauth-token-refresh-PHW66RC4.js +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AZDO_RESOURCE_ID,
|
|
4
|
+
CredentialMissingError,
|
|
5
|
+
CredentialStoreUnavailableError,
|
|
6
|
+
SETTINGS,
|
|
7
|
+
appendAuthAuditEvent,
|
|
8
|
+
buildScopeString,
|
|
9
|
+
defaultScopes,
|
|
10
|
+
deletePat,
|
|
11
|
+
firstPartyShippedScopes,
|
|
12
|
+
getConfigValue,
|
|
13
|
+
getPat,
|
|
14
|
+
getStoredCredential,
|
|
15
|
+
listOrgsWithStoredPat,
|
|
16
|
+
loadConfig,
|
|
17
|
+
maskedDisplay,
|
|
18
|
+
normalizePat,
|
|
19
|
+
openUrl,
|
|
20
|
+
probeBackend,
|
|
21
|
+
readAuditEvents,
|
|
22
|
+
readTokenResponse,
|
|
23
|
+
refreshIfNeeded,
|
|
24
|
+
resolveOAuthConfig,
|
|
25
|
+
runAuthCodeFlow,
|
|
26
|
+
setConfigValue,
|
|
27
|
+
storeOAuthCredential,
|
|
28
|
+
storePat,
|
|
29
|
+
tokenResponseToCredential,
|
|
30
|
+
unsetConfigValue
|
|
31
|
+
} from "./chunk-C7RAZJHV.js";
|
|
2
32
|
|
|
3
33
|
// src/index.ts
|
|
4
34
|
import { Command as Command15 } from "commander";
|
|
@@ -26,8 +56,15 @@ var DEFAULT_FIELDS = [
|
|
|
26
56
|
"System.AreaPath",
|
|
27
57
|
"System.IterationPath"
|
|
28
58
|
];
|
|
29
|
-
function authHeaders(
|
|
30
|
-
|
|
59
|
+
function authHeaders(credentialOrPat) {
|
|
60
|
+
if (typeof credentialOrPat === "string") {
|
|
61
|
+
const token2 = Buffer.from(`:${credentialOrPat}`).toString("base64");
|
|
62
|
+
return { Authorization: `Basic ${token2}` };
|
|
63
|
+
}
|
|
64
|
+
if (credentialOrPat.kind === "oauth") {
|
|
65
|
+
return { Authorization: `Bearer ${credentialOrPat.pat}` };
|
|
66
|
+
}
|
|
67
|
+
const token = Buffer.from(`:${credentialOrPat.pat}`).toString("base64");
|
|
31
68
|
return { Authorization: `Basic ${token}` };
|
|
32
69
|
}
|
|
33
70
|
async function fetchWithErrors(url, init) {
|
|
@@ -48,6 +85,10 @@ async function fetchWithErrors(url, init) {
|
|
|
48
85
|
}
|
|
49
86
|
throw new Error(`NOT_FOUND${detail}`);
|
|
50
87
|
}
|
|
88
|
+
const contentType = response.headers?.get("content-type") ?? "";
|
|
89
|
+
if (contentType.toLowerCase().startsWith("text/html")) {
|
|
90
|
+
throw new Error("AUTH_FAILED");
|
|
91
|
+
}
|
|
51
92
|
return response;
|
|
52
93
|
}
|
|
53
94
|
async function readResponseMessage(response) {
|
|
@@ -90,9 +131,9 @@ function buildExtraFields(fields, requested) {
|
|
|
90
131
|
}
|
|
91
132
|
return Object.keys(result).length > 0 ? result : null;
|
|
92
133
|
}
|
|
93
|
-
function writeHeaders(
|
|
134
|
+
function writeHeaders(cred) {
|
|
94
135
|
return {
|
|
95
|
-
...authHeaders(
|
|
136
|
+
...authHeaders(cred),
|
|
96
137
|
"Content-Type": "application/json-patch+json"
|
|
97
138
|
};
|
|
98
139
|
}
|
|
@@ -147,13 +188,13 @@ async function readWriteResponse(response, errorCode) {
|
|
|
147
188
|
fields: data.fields
|
|
148
189
|
};
|
|
149
190
|
}
|
|
150
|
-
async function getWorkItemFields(context, id,
|
|
191
|
+
async function getWorkItemFields(context, id, cred) {
|
|
151
192
|
const url = new URL(
|
|
152
193
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
153
194
|
);
|
|
154
195
|
url.searchParams.set("api-version", "7.1");
|
|
155
196
|
url.searchParams.set("$expand", "all");
|
|
156
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
197
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
157
198
|
if (response.status === 400) {
|
|
158
199
|
const serverMessage = await readResponseMessage(response);
|
|
159
200
|
if (serverMessage) {
|
|
@@ -188,10 +229,10 @@ function buildWorkItemUrl(context, id, options = {}) {
|
|
|
188
229
|
}
|
|
189
230
|
return url;
|
|
190
231
|
}
|
|
191
|
-
async function fetchWorkItemResponse(context, id,
|
|
232
|
+
async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
192
233
|
const response = await fetchWithErrors(
|
|
193
234
|
buildWorkItemUrl(context, id, options).toString(),
|
|
194
|
-
{ headers: authHeaders(
|
|
235
|
+
{ headers: authHeaders(cred) }
|
|
195
236
|
);
|
|
196
237
|
if (response.status === 400) {
|
|
197
238
|
const serverMessage = await readResponseMessage(response);
|
|
@@ -204,12 +245,12 @@ async function fetchWorkItemResponse(context, id, pat, options = {}) {
|
|
|
204
245
|
}
|
|
205
246
|
return await response.json();
|
|
206
247
|
}
|
|
207
|
-
async function getWorkItem(context, id,
|
|
248
|
+
async function getWorkItem(context, id, cred, extraFields) {
|
|
208
249
|
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
209
|
-
const data = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id,
|
|
250
|
+
const data = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, {
|
|
210
251
|
fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
|
|
211
|
-
}) : await fetchWorkItemResponse(context, id,
|
|
212
|
-
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id,
|
|
252
|
+
}) : await fetchWorkItemResponse(context, id, cred, { includeRelations: true });
|
|
253
|
+
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, { includeRelations: true }) : data;
|
|
213
254
|
const descriptionParts = [];
|
|
214
255
|
if (data.fields["System.Description"]) {
|
|
215
256
|
descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
|
|
@@ -241,13 +282,13 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
241
282
|
attachments: extractAttachments(relationsData.relations)
|
|
242
283
|
};
|
|
243
284
|
}
|
|
244
|
-
async function getWorkItemFieldValue(context, id,
|
|
285
|
+
async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
245
286
|
const url = new URL(
|
|
246
287
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
247
288
|
);
|
|
248
289
|
url.searchParams.set("api-version", "7.1");
|
|
249
290
|
url.searchParams.set("fields", fieldName);
|
|
250
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
291
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
251
292
|
if (response.status === 400) {
|
|
252
293
|
const serverMessage = await readResponseMessage(response);
|
|
253
294
|
if (serverMessage) {
|
|
@@ -264,13 +305,13 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
264
305
|
}
|
|
265
306
|
return stringifyFieldValue(value);
|
|
266
307
|
}
|
|
267
|
-
async function listWorkItemComments(context, id,
|
|
308
|
+
async function listWorkItemComments(context, id, cred) {
|
|
268
309
|
const comments = [];
|
|
269
310
|
let continuationToken = null;
|
|
270
311
|
do {
|
|
271
312
|
const response = await fetchWithErrors(
|
|
272
313
|
buildWorkItemCommentsListUrl(context, id, continuationToken ?? void 0).toString(),
|
|
273
|
-
{ headers: authHeaders(
|
|
314
|
+
{ headers: authHeaders(cred) }
|
|
274
315
|
);
|
|
275
316
|
if (!response.ok) {
|
|
276
317
|
throw new Error(`HTTP_${response.status}`);
|
|
@@ -287,13 +328,13 @@ async function listWorkItemComments(context, id, pat) {
|
|
|
287
328
|
comments
|
|
288
329
|
};
|
|
289
330
|
}
|
|
290
|
-
async function addWorkItemComment(context, id,
|
|
331
|
+
async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
291
332
|
const url = buildWorkItemCommentsUrl(context, id);
|
|
292
333
|
url.searchParams.set("format", format);
|
|
293
334
|
const response = await fetchWithErrors(url.toString(), {
|
|
294
335
|
method: "POST",
|
|
295
336
|
headers: {
|
|
296
|
-
...authHeaders(
|
|
337
|
+
...authHeaders(cred),
|
|
297
338
|
"Content-Type": "application/json"
|
|
298
339
|
},
|
|
299
340
|
body: JSON.stringify({ text })
|
|
@@ -315,8 +356,8 @@ async function addWorkItemComment(context, id, pat, text, format = "html") {
|
|
|
315
356
|
url: data.url ?? null
|
|
316
357
|
};
|
|
317
358
|
}
|
|
318
|
-
async function updateWorkItem(context, id,
|
|
319
|
-
const result = await applyWorkItemPatch(context, id,
|
|
359
|
+
async function updateWorkItem(context, id, cred, fieldName, operations) {
|
|
360
|
+
const result = await applyWorkItemPatch(context, id, cred, operations);
|
|
320
361
|
const title = result.fields["System.Title"];
|
|
321
362
|
const lastOp = operations.at(-1);
|
|
322
363
|
const fieldValue = lastOp?.value ?? null;
|
|
@@ -328,32 +369,32 @@ async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
|
328
369
|
fieldValue
|
|
329
370
|
};
|
|
330
371
|
}
|
|
331
|
-
async function createWorkItem(context, workItemType,
|
|
372
|
+
async function createWorkItem(context, workItemType, cred, operations) {
|
|
332
373
|
const url = new URL(
|
|
333
374
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}`
|
|
334
375
|
);
|
|
335
376
|
url.searchParams.set("api-version", "7.1");
|
|
336
377
|
const response = await fetchWithErrors(url.toString(), {
|
|
337
378
|
method: "POST",
|
|
338
|
-
headers: writeHeaders(
|
|
379
|
+
headers: writeHeaders(cred),
|
|
339
380
|
body: JSON.stringify(operations)
|
|
340
381
|
});
|
|
341
382
|
return readWriteResponse(response, "CREATE_REJECTED");
|
|
342
383
|
}
|
|
343
|
-
async function applyWorkItemPatch(context, id,
|
|
384
|
+
async function applyWorkItemPatch(context, id, cred, operations) {
|
|
344
385
|
const url = new URL(
|
|
345
386
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
346
387
|
);
|
|
347
388
|
url.searchParams.set("api-version", "7.1");
|
|
348
389
|
const response = await fetchWithErrors(url.toString(), {
|
|
349
390
|
method: "PATCH",
|
|
350
|
-
headers: writeHeaders(
|
|
391
|
+
headers: writeHeaders(cred),
|
|
351
392
|
body: JSON.stringify(operations)
|
|
352
393
|
});
|
|
353
394
|
return readWriteResponse(response, "UPDATE_REJECTED");
|
|
354
395
|
}
|
|
355
|
-
async function downloadAttachment(url,
|
|
356
|
-
const response = await fetchWithErrors(url, { headers: authHeaders(
|
|
396
|
+
async function downloadAttachment(url, cred) {
|
|
397
|
+
const response = await fetchWithErrors(url, { headers: authHeaders(cred) });
|
|
357
398
|
if (!response.ok) {
|
|
358
399
|
throw new Error(`HTTP_${response.status}`);
|
|
359
400
|
}
|
|
@@ -365,323 +406,124 @@ import { createInterface } from "readline";
|
|
|
365
406
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
366
407
|
import { dirname as dirname2, join } from "path";
|
|
367
408
|
|
|
368
|
-
// src/services/
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
super(`OS secret backend unavailable (${backend}). Install the platform's credential service and try again.`);
|
|
376
|
-
this.name = "CredentialStoreUnavailableError";
|
|
377
|
-
this.backend = backend;
|
|
409
|
+
// src/services/oauth-device-code.ts
|
|
410
|
+
var DeviceCodeFlowError = class extends Error {
|
|
411
|
+
reason;
|
|
412
|
+
constructor(reason, message, cause) {
|
|
413
|
+
super(message);
|
|
414
|
+
this.name = "DeviceCodeFlowError";
|
|
415
|
+
this.reason = reason;
|
|
378
416
|
if (cause instanceof Error) {
|
|
379
417
|
this.cause = cause;
|
|
380
418
|
}
|
|
381
419
|
}
|
|
382
420
|
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function ensureFileWithPerms(file) {
|
|
402
|
-
if (!fs.existsSync(file)) {
|
|
403
|
-
fs.writeFileSync(file, "", { mode: 384 });
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
421
|
+
var MIN_INTERVAL_SEC = 5;
|
|
422
|
+
async function defaultSleep(ms) {
|
|
423
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
424
|
+
}
|
|
425
|
+
async function requestDeviceCode(oauthConfig, fetchFn) {
|
|
426
|
+
const body = new URLSearchParams({
|
|
427
|
+
client_id: oauthConfig.clientId,
|
|
428
|
+
scope: buildScopeString(oauthConfig.scopes)
|
|
429
|
+
});
|
|
430
|
+
const response = await fetchFn(oauthConfig.deviceCodeEndpoint, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
433
|
+
body: body.toString()
|
|
434
|
+
});
|
|
435
|
+
const text = await response.text();
|
|
436
|
+
let parsed;
|
|
406
437
|
try {
|
|
407
|
-
|
|
438
|
+
parsed = JSON.parse(text);
|
|
408
439
|
} catch {
|
|
440
|
+
throw new DeviceCodeFlowError("idp-error", `device-code endpoint returned non-JSON HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
409
441
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
event: input.event,
|
|
419
|
-
org: input.org,
|
|
420
|
-
backend: input.backend,
|
|
421
|
-
...input.masked_pat !== void 0 ? { masked_pat: input.masked_pat } : {}
|
|
422
|
-
};
|
|
423
|
-
fs.appendFileSync(auditLog, `${JSON.stringify(record)}
|
|
424
|
-
`);
|
|
425
|
-
}
|
|
426
|
-
function readAuditEvents() {
|
|
427
|
-
const auditLog = getAuditLogPath();
|
|
428
|
-
if (!fs.existsSync(auditLog)) {
|
|
429
|
-
return [];
|
|
430
|
-
}
|
|
431
|
-
const contents = fs.readFileSync(auditLog, "utf8");
|
|
432
|
-
const out = [];
|
|
433
|
-
for (const line of contents.split("\n")) {
|
|
434
|
-
const trimmed = line.trim();
|
|
435
|
-
if (!trimmed) continue;
|
|
436
|
-
try {
|
|
437
|
-
const parsed = JSON.parse(trimmed);
|
|
438
|
-
if (parsed && typeof parsed === "object" && typeof parsed.event === "string") {
|
|
439
|
-
out.push(parsed);
|
|
440
|
-
}
|
|
441
|
-
} catch {
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return out;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// src/services/config-store.ts
|
|
448
|
-
import fs2 from "fs";
|
|
449
|
-
import path2 from "path";
|
|
450
|
-
import os2 from "os";
|
|
451
|
-
var SETTINGS = [
|
|
452
|
-
{
|
|
453
|
-
key: "org",
|
|
454
|
-
description: "Azure DevOps organization name",
|
|
455
|
-
type: "string",
|
|
456
|
-
example: "mycompany",
|
|
457
|
-
required: true
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
key: "project",
|
|
461
|
-
description: "Azure DevOps project name",
|
|
462
|
-
type: "string",
|
|
463
|
-
example: "MyProject",
|
|
464
|
-
required: true
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
key: "fields",
|
|
468
|
-
description: "Extra work item fields to include (comma-separated reference names)",
|
|
469
|
-
type: "string[]",
|
|
470
|
-
example: "System.Tags,Custom.Priority",
|
|
471
|
-
required: false
|
|
472
|
-
},
|
|
473
|
-
{
|
|
474
|
-
key: "markdown",
|
|
475
|
-
description: "Convert rich text fields to markdown on display",
|
|
476
|
-
type: "boolean",
|
|
477
|
-
example: "true",
|
|
478
|
-
required: false
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const err = parsed;
|
|
444
|
+
const code = err.error ?? "unknown";
|
|
445
|
+
const desc = err.error_description ? `: ${err.error_description}` : "";
|
|
446
|
+
throw new DeviceCodeFlowError(
|
|
447
|
+
"idp-error",
|
|
448
|
+
`device-code endpoint rejected request (${response.status}): ${code}${desc}`
|
|
449
|
+
);
|
|
479
450
|
}
|
|
480
|
-
|
|
481
|
-
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
482
|
-
function getConfigPath() {
|
|
483
|
-
return path2.join(os2.homedir(), ".azdo", "config.json");
|
|
451
|
+
return parsed;
|
|
484
452
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
raw = fs2.readFileSync(configPath, "utf-8");
|
|
490
|
-
} catch (err) {
|
|
491
|
-
if (err.code === "ENOENT") {
|
|
492
|
-
return {};
|
|
493
|
-
}
|
|
494
|
-
throw err;
|
|
453
|
+
async function classifyDeviceTokenResponse(response) {
|
|
454
|
+
if (response.ok) {
|
|
455
|
+
return { kind: "success", token: await readTokenResponse(response) };
|
|
495
456
|
}
|
|
457
|
+
const text = await response.text();
|
|
458
|
+
let parsed;
|
|
496
459
|
try {
|
|
497
|
-
|
|
460
|
+
parsed = JSON.parse(text);
|
|
498
461
|
} catch {
|
|
499
|
-
|
|
500
|
-
`);
|
|
501
|
-
return {};
|
|
462
|
+
throw new DeviceCodeFlowError("idp-error", `non-JSON HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
502
463
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
509
|
-
}
|
|
510
|
-
function validateKey(key) {
|
|
511
|
-
if (!VALID_KEYS.includes(key)) {
|
|
512
|
-
throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
464
|
+
const errCode = parsed.error ?? "";
|
|
465
|
+
if (errCode === "authorization_pending") return { kind: "pending" };
|
|
466
|
+
if (errCode === "slow_down") return { kind: "slow_down" };
|
|
467
|
+
if (errCode === "expired_token") {
|
|
468
|
+
throw new DeviceCodeFlowError("expired_token", "device code expired before authorisation completed");
|
|
513
469
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
validateKey(key);
|
|
517
|
-
const config = loadConfig();
|
|
518
|
-
return config[key];
|
|
519
|
-
}
|
|
520
|
-
function setConfigValue(key, value) {
|
|
521
|
-
validateKey(key);
|
|
522
|
-
const config = loadConfig();
|
|
523
|
-
if (value === "") {
|
|
524
|
-
delete config[key];
|
|
525
|
-
} else if (key === "markdown") {
|
|
526
|
-
if (value !== "true" && value !== "false") {
|
|
527
|
-
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
528
|
-
}
|
|
529
|
-
config.markdown = value === "true";
|
|
530
|
-
} else if (key === "fields") {
|
|
531
|
-
config.fields = value.split(",").map((s) => s.trim());
|
|
532
|
-
} else {
|
|
533
|
-
config[key] = value;
|
|
534
|
-
}
|
|
535
|
-
saveConfig(config);
|
|
536
|
-
}
|
|
537
|
-
function unsetConfigValue(key) {
|
|
538
|
-
validateKey(key);
|
|
539
|
-
const config = loadConfig();
|
|
540
|
-
delete config[key];
|
|
541
|
-
saveConfig(config);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// src/services/auth-masking.ts
|
|
545
|
-
var VISIBLE_CHARS = 5;
|
|
546
|
-
function maskedDisplay(pat) {
|
|
547
|
-
if (pat.length <= VISIBLE_CHARS * 2) {
|
|
548
|
-
return pat;
|
|
549
|
-
}
|
|
550
|
-
const hiddenCount = pat.length - VISIBLE_CHARS * 2;
|
|
551
|
-
return pat.slice(0, VISIBLE_CHARS) + "*".repeat(hiddenCount) + pat.slice(-VISIBLE_CHARS);
|
|
552
|
-
}
|
|
553
|
-
function normalizePat(rawPat) {
|
|
554
|
-
const trimmedPat = rawPat.trim();
|
|
555
|
-
return trimmedPat.length > 0 ? trimmedPat : null;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// src/services/credential-store.ts
|
|
559
|
-
var SERVICE = "azdo-cli";
|
|
560
|
-
var LEGACY_ACCOUNT = "pat";
|
|
561
|
-
function accountFor(org) {
|
|
562
|
-
return `pat:${org}`;
|
|
563
|
-
}
|
|
564
|
-
function probeBackend() {
|
|
565
|
-
switch (process.platform) {
|
|
566
|
-
case "win32":
|
|
567
|
-
return "windows-credential-manager";
|
|
568
|
-
case "darwin":
|
|
569
|
-
return "macos-keychain";
|
|
570
|
-
case "linux":
|
|
571
|
-
return "linux-libsecret";
|
|
572
|
-
default:
|
|
573
|
-
return "unknown";
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function wrapUnavailable(fn) {
|
|
577
|
-
try {
|
|
578
|
-
return fn();
|
|
579
|
-
} catch (err) {
|
|
580
|
-
throw new CredentialStoreUnavailableError(probeBackend(), err);
|
|
470
|
+
if (errCode === "access_denied") {
|
|
471
|
+
throw new DeviceCodeFlowError("access_denied", "authorisation denied by user");
|
|
581
472
|
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
legacyUnsetNoticeEmitted = true;
|
|
587
|
-
process.stderr.write(
|
|
588
|
-
'A legacy PAT exists in the OS vault from a previous azdo-cli version, but no "org" is set in config. Run `azdo auth --org <name>` to re-store it under the per-org key, then `azdo clear-pat` to remove the legacy slot.\n'
|
|
473
|
+
const desc = parsed.error_description ? `: ${parsed.error_description}` : "";
|
|
474
|
+
throw new DeviceCodeFlowError(
|
|
475
|
+
"idp-error",
|
|
476
|
+
`IdP rejected device-token poll (${response.status}): ${errCode}${desc}`
|
|
589
477
|
);
|
|
590
478
|
}
|
|
591
|
-
async function
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
479
|
+
async function pollForDeviceToken(deviceCode, oauthConfig, initialIntervalSec, expiresAtMs, deps) {
|
|
480
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
481
|
+
const now = deps.now ?? (() => Date.now());
|
|
482
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
483
|
+
let intervalSec = Math.max(MIN_INTERVAL_SEC, initialIntervalSec);
|
|
484
|
+
for (; ; ) {
|
|
485
|
+
if (now() >= expiresAtMs) {
|
|
486
|
+
throw new DeviceCodeFlowError("expired_token", "device-code flow expired before authorisation completed");
|
|
487
|
+
}
|
|
488
|
+
await sleep(intervalSec * 1e3);
|
|
489
|
+
const body = new URLSearchParams({
|
|
490
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
491
|
+
client_id: oauthConfig.clientId,
|
|
492
|
+
device_code: deviceCode
|
|
493
|
+
});
|
|
494
|
+
const response = await fetchFn(oauthConfig.tokenEndpoint, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
497
|
+
body: body.toString()
|
|
498
|
+
});
|
|
499
|
+
const outcome = await classifyDeviceTokenResponse(response);
|
|
500
|
+
if (outcome.kind === "success") return outcome.token;
|
|
501
|
+
if (outcome.kind === "slow_down") {
|
|
502
|
+
intervalSec += 5;
|
|
605
503
|
}
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
const newEntry = new Entry(SERVICE, accountFor(targetOrg));
|
|
609
|
-
const existingNew = wrapUnavailable(() => newEntry.getPassword());
|
|
610
|
-
if (existingNew !== null) {
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
const legacyEntry = new Entry(SERVICE, LEGACY_ACCOUNT);
|
|
614
|
-
const legacy = wrapUnavailable(() => legacyEntry.getPassword());
|
|
615
|
-
if (legacy === null) {
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
wrapUnavailable(() => {
|
|
619
|
-
newEntry.setPassword(legacy);
|
|
620
|
-
legacyEntry.deletePassword();
|
|
621
|
-
});
|
|
622
|
-
appendAuthAuditEvent({
|
|
623
|
-
event: "auth.store",
|
|
624
|
-
org: targetOrg,
|
|
625
|
-
backend: probeBackend(),
|
|
626
|
-
masked_pat: maskedDisplay(legacy)
|
|
627
|
-
});
|
|
628
|
-
process.stderr.write(`Migrated legacy PAT to org ${targetOrg}.
|
|
629
|
-
`);
|
|
630
|
-
return legacy;
|
|
631
|
-
}
|
|
632
|
-
async function getPat(org) {
|
|
633
|
-
const entry = new Entry(SERVICE, accountFor(org));
|
|
634
|
-
const value = wrapUnavailable(() => entry.getPassword());
|
|
635
|
-
if (value !== null) {
|
|
636
|
-
return value;
|
|
637
504
|
}
|
|
638
|
-
const migrated = await maybeMigrateLegacy(org);
|
|
639
|
-
return migrated;
|
|
640
505
|
}
|
|
641
|
-
async function
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
org,
|
|
647
|
-
backend: probeBackend(),
|
|
648
|
-
masked_pat: maskedDisplay(pat)
|
|
506
|
+
async function runDeviceCodeFlow(org, oauthConfig, deps = {}) {
|
|
507
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
508
|
+
const now = deps.now ?? (() => Date.now());
|
|
509
|
+
const writePrompt = deps.writePrompt ?? ((m) => {
|
|
510
|
+
process.stderr.write(m);
|
|
649
511
|
});
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return
|
|
665
|
-
}
|
|
666
|
-
async function listOrgsWithStoredPat() {
|
|
667
|
-
const seen = /* @__PURE__ */ new Set();
|
|
668
|
-
for (const ev of readAuditEvents()) {
|
|
669
|
-
if (ev.event === "auth.store") {
|
|
670
|
-
seen.add(ev.org);
|
|
671
|
-
} else if (ev.event === "auth.delete") {
|
|
672
|
-
seen.delete(ev.org);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const present = [];
|
|
676
|
-
for (const org of seen) {
|
|
677
|
-
const entry = new Entry(SERVICE, accountFor(org));
|
|
678
|
-
const value = wrapUnavailable(() => entry.getPassword());
|
|
679
|
-
if (value !== null) {
|
|
680
|
-
present.push(org);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
present.sort((a, b) => a.localeCompare(b));
|
|
684
|
-
return present;
|
|
512
|
+
const dc = await requestDeviceCode(oauthConfig, fetchFn);
|
|
513
|
+
const expiryMin = Math.round(dc.expires_in / 60);
|
|
514
|
+
writePrompt(
|
|
515
|
+
`
|
|
516
|
+
To authenticate, open ${dc.verification_uri} in a browser and enter the code:
|
|
517
|
+
|
|
518
|
+
${dc.user_code}
|
|
519
|
+
|
|
520
|
+
Waiting for authorisation (expires in ${expiryMin} min)\u2026
|
|
521
|
+
`
|
|
522
|
+
);
|
|
523
|
+
const expiresAtMs = now() + dc.expires_in * 1e3;
|
|
524
|
+
const token = await pollForDeviceToken(dc.device_code, oauthConfig, dc.interval, expiresAtMs, deps);
|
|
525
|
+
const credential = tokenResponseToCredential(org, oauthConfig, token, now());
|
|
526
|
+
return { credential, flowUsed: "device-code" };
|
|
685
527
|
}
|
|
686
528
|
|
|
687
529
|
// src/services/auth.ts
|
|
@@ -750,29 +592,36 @@ function findDotEnvPat(startDir = process.cwd()) {
|
|
|
750
592
|
}
|
|
751
593
|
return null;
|
|
752
594
|
}
|
|
753
|
-
async function
|
|
595
|
+
async function resolveAuthCredential(org) {
|
|
754
596
|
const envPat = process.env.AZDO_PAT;
|
|
755
597
|
if (envPat && envPat.length > 0) {
|
|
756
|
-
return { pat: envPat, source: "env" };
|
|
598
|
+
return { pat: envPat, source: "env", kind: "pat" };
|
|
757
599
|
}
|
|
758
|
-
const
|
|
759
|
-
if (
|
|
760
|
-
|
|
600
|
+
const stored = await getStoredCredential(org);
|
|
601
|
+
if (stored !== null) {
|
|
602
|
+
if (stored.kind === "pat") {
|
|
603
|
+
return { pat: stored.token, source: "credential-store", kind: "pat" };
|
|
604
|
+
}
|
|
605
|
+
const fresh = await refreshIfNeeded(org, stored);
|
|
606
|
+
return {
|
|
607
|
+
pat: fresh.accessToken,
|
|
608
|
+
source: "credential-store",
|
|
609
|
+
kind: "oauth",
|
|
610
|
+
accountId: fresh.accountId
|
|
611
|
+
};
|
|
761
612
|
}
|
|
762
613
|
const dotEnvPat = findDotEnvPat();
|
|
763
614
|
if (dotEnvPat !== null) {
|
|
764
|
-
return { pat: dotEnvPat, source: "env" };
|
|
615
|
+
return { pat: dotEnvPat, source: "env", kind: "pat" };
|
|
765
616
|
}
|
|
766
617
|
return null;
|
|
767
618
|
}
|
|
768
|
-
async function
|
|
769
|
-
const cred = await
|
|
619
|
+
async function requireAuthCredential(org) {
|
|
620
|
+
const cred = await resolveAuthCredential(org);
|
|
770
621
|
if (cred !== null) {
|
|
771
622
|
return cred;
|
|
772
623
|
}
|
|
773
|
-
throw new
|
|
774
|
-
`No PAT available for org "${org}". Set AZDO_PAT environment variable or run \`azdo auth --org ${org}\`.`
|
|
775
|
-
);
|
|
624
|
+
throw new CredentialMissingError(org);
|
|
776
625
|
}
|
|
777
626
|
async function validatePatAgainstAzdo(pat, org) {
|
|
778
627
|
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?$top=1&api-version=7.1`;
|
|
@@ -791,25 +640,168 @@ async function validatePatAgainstAzdo(pat, org) {
|
|
|
791
640
|
}
|
|
792
641
|
throw new Error(`Azure DevOps returned HTTP ${response.status} while validating PAT for org "${org}".`);
|
|
793
642
|
}
|
|
643
|
+
var LOGIN_FAILURE_REASONS = /* @__PURE__ */ new Set([
|
|
644
|
+
"user-cancelled",
|
|
645
|
+
"port-conflict",
|
|
646
|
+
"state-mismatch",
|
|
647
|
+
"redirect-mismatch",
|
|
648
|
+
"idp-error",
|
|
649
|
+
"timeout",
|
|
650
|
+
"expired_token",
|
|
651
|
+
"access_denied"
|
|
652
|
+
]);
|
|
653
|
+
function extractLoginFailureReason(err) {
|
|
654
|
+
if (typeof err === "object" && err !== null && "reason" in err) {
|
|
655
|
+
const r = err.reason;
|
|
656
|
+
if (typeof r === "string" && LOGIN_FAILURE_REASONS.has(r)) {
|
|
657
|
+
return r;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return "unknown";
|
|
661
|
+
}
|
|
662
|
+
async function loginWithOAuth(org, opts = {}) {
|
|
663
|
+
const oauthConfig = resolveOAuthConfig({
|
|
664
|
+
clientIdOverride: opts.clientIdOverride,
|
|
665
|
+
tenantIdOverride: opts.tenantIdOverride,
|
|
666
|
+
scopesOverride: opts.scopesOverride
|
|
667
|
+
});
|
|
668
|
+
const isHeadlessRuntime = () => {
|
|
669
|
+
if (opts.forceHeadless) return true;
|
|
670
|
+
if (process.platform === "linux") {
|
|
671
|
+
return !process.env.DISPLAY || process.env.DISPLAY.length === 0;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
};
|
|
675
|
+
const useDeviceCode = opts.flow === "device-code" || opts.flow !== "auth-code" && isHeadlessRuntime();
|
|
676
|
+
appendAuthAuditEvent({
|
|
677
|
+
event: "oauth-login-started",
|
|
678
|
+
org,
|
|
679
|
+
backend: probeBackend(),
|
|
680
|
+
flow: useDeviceCode ? "device-code" : "auth-code",
|
|
681
|
+
clientIdSource: oauthConfig.clientIdSource
|
|
682
|
+
});
|
|
683
|
+
let credential;
|
|
684
|
+
let flowUsed;
|
|
685
|
+
try {
|
|
686
|
+
if (useDeviceCode) {
|
|
687
|
+
const r = await runDeviceCodeFlow(org, oauthConfig);
|
|
688
|
+
credential = r.credential;
|
|
689
|
+
flowUsed = "device-code";
|
|
690
|
+
} else {
|
|
691
|
+
const r = await runAuthCodeFlow(org, oauthConfig);
|
|
692
|
+
credential = r.credential;
|
|
693
|
+
flowUsed = "auth-code";
|
|
694
|
+
}
|
|
695
|
+
} catch (err) {
|
|
696
|
+
appendAuthAuditEvent({
|
|
697
|
+
event: "oauth-login-failed",
|
|
698
|
+
org,
|
|
699
|
+
backend: probeBackend(),
|
|
700
|
+
flow: useDeviceCode ? "device-code" : "auth-code",
|
|
701
|
+
reason: extractLoginFailureReason(err)
|
|
702
|
+
});
|
|
703
|
+
throw err;
|
|
704
|
+
}
|
|
705
|
+
await storeOAuthCredential(org, credential);
|
|
706
|
+
appendAuthAuditEvent({
|
|
707
|
+
event: "oauth-login-success",
|
|
708
|
+
org,
|
|
709
|
+
backend: probeBackend(),
|
|
710
|
+
flow: flowUsed,
|
|
711
|
+
clientIdSource: oauthConfig.clientIdSource,
|
|
712
|
+
accountId: credential.accountId,
|
|
713
|
+
scope: credential.scope,
|
|
714
|
+
tokenLifetimeSec: credential.expiresAt - credential.issuedAt
|
|
715
|
+
});
|
|
716
|
+
return {
|
|
717
|
+
org,
|
|
718
|
+
kind: "oauth",
|
|
719
|
+
accountId: credential.accountId,
|
|
720
|
+
expiresAt: credential.expiresAt,
|
|
721
|
+
scope: credential.scope,
|
|
722
|
+
flowUsed
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
async function logout(opts = {}) {
|
|
726
|
+
if (opts.all) {
|
|
727
|
+
const orgs = await listOrgsWithStoredPat();
|
|
728
|
+
const removed = [];
|
|
729
|
+
for (const o of orgs) {
|
|
730
|
+
const cred2 = await getStoredCredential(o);
|
|
731
|
+
const ok2 = await deletePat(o);
|
|
732
|
+
if (ok2 && cred2 !== null) {
|
|
733
|
+
removed.push({ org: o, kind: cred2.kind });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return { removed };
|
|
737
|
+
}
|
|
738
|
+
if (!opts.org) {
|
|
739
|
+
throw new Error("logout requires an org or --all");
|
|
740
|
+
}
|
|
741
|
+
const cred = await getStoredCredential(opts.org);
|
|
742
|
+
const ok = await deletePat(opts.org);
|
|
743
|
+
return { removed: ok && cred !== null ? [{ org: opts.org, kind: cred.kind }] : [] };
|
|
744
|
+
}
|
|
745
|
+
async function status() {
|
|
746
|
+
const orgs = await listOrgsWithStoredPat();
|
|
747
|
+
const out = [];
|
|
748
|
+
for (const org of orgs) {
|
|
749
|
+
const cred = await getStoredCredential(org);
|
|
750
|
+
if (cred === null) continue;
|
|
751
|
+
if (cred.kind === "pat") {
|
|
752
|
+
out.push({ org, kind: "pat", backend: probeBackend() });
|
|
753
|
+
} else {
|
|
754
|
+
out.push({
|
|
755
|
+
org,
|
|
756
|
+
kind: "oauth",
|
|
757
|
+
accountId: cred.accountId,
|
|
758
|
+
expiresAt: cred.expiresAt,
|
|
759
|
+
scope: cred.scope,
|
|
760
|
+
backend: probeBackend()
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { orgs: out };
|
|
765
|
+
}
|
|
794
766
|
|
|
795
767
|
// src/services/git-remote.ts
|
|
796
768
|
import { execSync } from "child_process";
|
|
769
|
+
|
|
770
|
+
// src/services/remote-warning.ts
|
|
771
|
+
var WARNING = "azdo: warning: origin includes embedded credentials; consider removing them with 'git remote set-url origin <clean-url>'\n";
|
|
772
|
+
var warned = false;
|
|
773
|
+
function noticeCredentialBearingRemote() {
|
|
774
|
+
if (warned) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
warned = true;
|
|
778
|
+
try {
|
|
779
|
+
process.stderr.write(WARNING);
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/services/git-remote.ts
|
|
797
785
|
var patterns = [
|
|
798
|
-
// HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
|
|
799
|
-
/^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]
|
|
800
|
-
// HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
|
|
801
|
-
/^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/([^/]
|
|
802
|
-
// HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
|
|
803
|
-
/^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]
|
|
804
|
-
// SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
|
|
805
|
-
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]
|
|
806
|
-
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
|
|
807
|
-
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]
|
|
786
|
+
// HTTPS (current): https://[user[:token]@]dev.azure.com/{org}/{project}/_git/{repo}[.git]
|
|
787
|
+
/^https?:\/\/(?:[^@/]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/,
|
|
788
|
+
// HTTPS (legacy + DefaultCollection): https://[user[:token]@]{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}[.git]
|
|
789
|
+
/^https?:\/\/(?:[^@/]+@)?([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/,
|
|
790
|
+
// HTTPS (legacy): https://[user[:token]@]{org}.visualstudio.com/{project}/_git/{repo}[.git]
|
|
791
|
+
/^https?:\/\/(?:[^@/]+@)?([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/,
|
|
792
|
+
// SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}[.git]
|
|
793
|
+
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
794
|
+
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}[.git]
|
|
795
|
+
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/
|
|
808
796
|
];
|
|
797
|
+
var httpsUserinfo = /^https?:\/\/[^@/]+@/;
|
|
809
798
|
function parseAzdoRemote(url) {
|
|
810
799
|
for (const pattern of patterns) {
|
|
811
800
|
const match = pattern.exec(url);
|
|
812
801
|
if (match) {
|
|
802
|
+
if (httpsUserinfo.test(url)) {
|
|
803
|
+
noticeCredentialBearingRemote();
|
|
804
|
+
}
|
|
813
805
|
const project = match[2];
|
|
814
806
|
if (/^DefaultCollection$/i.test(project)) {
|
|
815
807
|
return { org: match[1], project: "" };
|
|
@@ -836,6 +828,9 @@ function parseRepoName(url) {
|
|
|
836
828
|
for (const pattern of patterns) {
|
|
837
829
|
const match = pattern.exec(url);
|
|
838
830
|
if (match) {
|
|
831
|
+
if (httpsUserinfo.test(url)) {
|
|
832
|
+
noticeCredentialBearingRemote();
|
|
833
|
+
}
|
|
839
834
|
return match[3];
|
|
840
835
|
}
|
|
841
836
|
}
|
|
@@ -1073,12 +1068,12 @@ function stripHtml(html) {
|
|
|
1073
1068
|
text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
|
|
1074
1069
|
text = text.replaceAll(/<li>/gi, "\n");
|
|
1075
1070
|
text = removeHtmlTags(text);
|
|
1076
|
-
text = text.replaceAll("&", "&");
|
|
1077
1071
|
text = text.replaceAll("<", "<");
|
|
1078
1072
|
text = text.replaceAll(">", ">");
|
|
1079
1073
|
text = text.replaceAll(""", '"');
|
|
1080
1074
|
text = text.replaceAll("'", "'");
|
|
1081
1075
|
text = text.replaceAll(" ", " ");
|
|
1076
|
+
text = text.replaceAll("&", "&");
|
|
1082
1077
|
text = text.replaceAll(/\n{3,}/g, "\n\n");
|
|
1083
1078
|
return text.trim();
|
|
1084
1079
|
}
|
|
@@ -1188,9 +1183,9 @@ function createGetItemCommand() {
|
|
|
1188
1183
|
let context;
|
|
1189
1184
|
try {
|
|
1190
1185
|
context = resolveContext(options);
|
|
1191
|
-
const credential = await
|
|
1186
|
+
const credential = await requireAuthCredential(context.org);
|
|
1192
1187
|
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
1193
|
-
const workItem = await getWorkItem(context, id, credential
|
|
1188
|
+
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1194
1189
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
1195
1190
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1196
1191
|
process.stdout.write(output + "\n");
|
|
@@ -1229,63 +1224,6 @@ function createClearPatCommand() {
|
|
|
1229
1224
|
|
|
1230
1225
|
// src/commands/auth.ts
|
|
1231
1226
|
import { Command as Command3 } from "commander";
|
|
1232
|
-
|
|
1233
|
-
// src/services/browser-open.ts
|
|
1234
|
-
import { execFile } from "child_process";
|
|
1235
|
-
function isHeadless(platform, hasDisplay) {
|
|
1236
|
-
if (platform === "linux") {
|
|
1237
|
-
return !hasDisplay;
|
|
1238
|
-
}
|
|
1239
|
-
return false;
|
|
1240
|
-
}
|
|
1241
|
-
function commandForPlatform(platform) {
|
|
1242
|
-
switch (platform) {
|
|
1243
|
-
case "darwin":
|
|
1244
|
-
return { cmd: "open", args: (url) => [url] };
|
|
1245
|
-
case "win32":
|
|
1246
|
-
return { cmd: "cmd", args: (url) => ["/c", "start", '""', url] };
|
|
1247
|
-
case "linux":
|
|
1248
|
-
return { cmd: "xdg-open", args: (url) => [url] };
|
|
1249
|
-
default:
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
async function openUrl(url, opts = {}) {
|
|
1254
|
-
const platform = opts.platform ?? process.platform;
|
|
1255
|
-
const hasDisplay = opts.hasDisplay ?? (process.env.DISPLAY !== void 0 && process.env.DISPLAY !== "");
|
|
1256
|
-
const forcePrint = opts.forcePrint ?? false;
|
|
1257
|
-
if (forcePrint || isHeadless(platform, hasDisplay)) {
|
|
1258
|
-
process.stderr.write(`Open this URL in your browser: ${url}
|
|
1259
|
-
`);
|
|
1260
|
-
return "printed";
|
|
1261
|
-
}
|
|
1262
|
-
const spec = commandForPlatform(platform);
|
|
1263
|
-
if (!spec) {
|
|
1264
|
-
process.stderr.write(`Open this URL in your browser: ${url}
|
|
1265
|
-
`);
|
|
1266
|
-
return "printed";
|
|
1267
|
-
}
|
|
1268
|
-
const runner = opts.execFileFn ?? ((cmd, args, cb) => execFile(cmd, args, { timeout: 5e3 }, (err) => cb(err)));
|
|
1269
|
-
return await new Promise((resolve2) => {
|
|
1270
|
-
try {
|
|
1271
|
-
runner(spec.cmd, spec.args(url), (err) => {
|
|
1272
|
-
if (err) {
|
|
1273
|
-
process.stderr.write(`Open this URL in your browser: ${url}
|
|
1274
|
-
`);
|
|
1275
|
-
resolve2("printed");
|
|
1276
|
-
} else {
|
|
1277
|
-
resolve2("opened");
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
} catch {
|
|
1281
|
-
process.stderr.write(`Open this URL in your browser: ${url}
|
|
1282
|
-
`);
|
|
1283
|
-
resolve2("printed");
|
|
1284
|
-
}
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// src/commands/auth.ts
|
|
1289
1227
|
async function readStdinToString() {
|
|
1290
1228
|
const chunks = [];
|
|
1291
1229
|
for await (const chunk of process.stdin) {
|
|
@@ -1293,9 +1231,9 @@ async function readStdinToString() {
|
|
|
1293
1231
|
}
|
|
1294
1232
|
return Buffer.concat(chunks).toString("utf8");
|
|
1295
1233
|
}
|
|
1296
|
-
async function
|
|
1234
|
+
async function promptYesNo(prompt) {
|
|
1297
1235
|
if (!process.stdin.isTTY) return true;
|
|
1298
|
-
process.stderr.write(
|
|
1236
|
+
process.stderr.write(prompt);
|
|
1299
1237
|
return await new Promise((resolve2) => {
|
|
1300
1238
|
process.stdin.setEncoding("utf8");
|
|
1301
1239
|
let answered = false;
|
|
@@ -1311,7 +1249,23 @@ async function confirmOverwrite(org) {
|
|
|
1311
1249
|
process.stdin.on("data", handler);
|
|
1312
1250
|
});
|
|
1313
1251
|
}
|
|
1314
|
-
async function
|
|
1252
|
+
async function confirmOverwrite(org) {
|
|
1253
|
+
return promptYesNo(`A PAT is already stored for org ${org}. Overwrite? [y/N] `);
|
|
1254
|
+
}
|
|
1255
|
+
async function confirmOverwriteCredential(org, existingKind) {
|
|
1256
|
+
const label = existingKind === "oauth" ? "OAuth credential" : "PAT";
|
|
1257
|
+
return promptYesNo(`A ${label} is already stored for org ${org}. The new login will replace it. Continue? [y/N] `);
|
|
1258
|
+
}
|
|
1259
|
+
function rejectMutuallyExclusive(opts) {
|
|
1260
|
+
if (opts.usePat && opts.deviceCode) {
|
|
1261
|
+
return "--use-pat and --device-code are mutually exclusive (PAT has no device-code flow).";
|
|
1262
|
+
}
|
|
1263
|
+
if (opts.usePat && (opts.clientId || opts.tenantId || opts.scopes)) {
|
|
1264
|
+
return "--use-pat cannot be combined with OAuth-only flags (--client-id / --tenant-id / --scopes).";
|
|
1265
|
+
}
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
async function handlePatLogin(options) {
|
|
1315
1269
|
const resolved = resolveOrg({ org: options.org });
|
|
1316
1270
|
if (!resolved) {
|
|
1317
1271
|
process.stderr.write(`${formatResolutionError()}
|
|
@@ -1377,7 +1331,145 @@ async function handleAuthRoot(options) {
|
|
|
1377
1331
|
process.stdout.write(`PAT stored for org ${org} in ${probeBackend()}.
|
|
1378
1332
|
`);
|
|
1379
1333
|
}
|
|
1334
|
+
async function ensureOverwriteConfirmed(org) {
|
|
1335
|
+
try {
|
|
1336
|
+
const existing = await getStoredCredential(org);
|
|
1337
|
+
if (existing === null || !process.stdin.isTTY) return "ok";
|
|
1338
|
+
const ok = await confirmOverwriteCredential(org, existing.kind);
|
|
1339
|
+
return ok ? "ok" : "aborted";
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1342
|
+
process.stderr.write(`${err.message}
|
|
1343
|
+
`);
|
|
1344
|
+
return "unavailable";
|
|
1345
|
+
}
|
|
1346
|
+
throw err;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function buildOAuthLoginOptions(options) {
|
|
1350
|
+
return {
|
|
1351
|
+
flow: options.deviceCode ? "device-code" : "auto",
|
|
1352
|
+
clientIdOverride: options.clientId,
|
|
1353
|
+
tenantIdOverride: options.tenantId,
|
|
1354
|
+
scopesOverride: options.scopes ? options.scopes.split(/\s+/).filter(Boolean) : void 0
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function reportOAuthFailure(err) {
|
|
1358
|
+
const reason = typeof err === "object" && err !== null && "reason" in err ? err.reason : null;
|
|
1359
|
+
const msg = err.message;
|
|
1360
|
+
process.stderr.write(reason ? `OAuth login failed (${reason}): ${msg}
|
|
1361
|
+
` : `OAuth login failed: ${msg}
|
|
1362
|
+
`);
|
|
1363
|
+
const noDisplay = process.platform === "linux" && (!process.env.DISPLAY || process.env.DISPLAY.length === 0);
|
|
1364
|
+
if (noDisplay) {
|
|
1365
|
+
process.stderr.write("Tip: this host has no DISPLAY; pass --device-code to use the headless flow.\n");
|
|
1366
|
+
} else if (reason === "port-conflict") {
|
|
1367
|
+
process.stderr.write("Tip: another process is using the loopback callback port. Try again or pass --device-code.\n");
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
async function handleOAuthLogin(options) {
|
|
1371
|
+
const resolved = resolveOrg({ org: options.org });
|
|
1372
|
+
if (!resolved) {
|
|
1373
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1374
|
+
`);
|
|
1375
|
+
process.exitCode = 3;
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const org = resolved.org;
|
|
1379
|
+
const confirm = await ensureOverwriteConfirmed(org);
|
|
1380
|
+
if (confirm === "aborted") {
|
|
1381
|
+
process.stderr.write("Aborted. Existing credential preserved.\n");
|
|
1382
|
+
process.exitCode = 1;
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (confirm === "unavailable") {
|
|
1386
|
+
process.exitCode = 4;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const result = await loginWithOAuth(org, buildOAuthLoginOptions(options));
|
|
1391
|
+
process.stdout.write(
|
|
1392
|
+
`Logged in to ${org} via OAuth (${result.flowUsed}). Account: ${result.accountId}; expires ${new Date(result.expiresAt * 1e3).toISOString()}.
|
|
1393
|
+
`
|
|
1394
|
+
);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
reportOAuthFailure(err);
|
|
1397
|
+
process.exitCode = 1;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async function handleAuthRoot(options) {
|
|
1401
|
+
const conflict = rejectMutuallyExclusive(options);
|
|
1402
|
+
if (conflict) {
|
|
1403
|
+
process.stderr.write(`${conflict}
|
|
1404
|
+
`);
|
|
1405
|
+
process.exitCode = 2;
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
await handlePatLogin(options);
|
|
1409
|
+
}
|
|
1410
|
+
async function handleLoginSubcommand(options) {
|
|
1411
|
+
const conflict = rejectMutuallyExclusive(options);
|
|
1412
|
+
if (conflict) {
|
|
1413
|
+
process.stderr.write(`${conflict}
|
|
1414
|
+
`);
|
|
1415
|
+
process.exitCode = 2;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (options.fromStdin || options.usePat) {
|
|
1419
|
+
await handlePatLogin(options);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
await handleOAuthLogin(options);
|
|
1423
|
+
}
|
|
1424
|
+
async function handleStatusJson() {
|
|
1425
|
+
try {
|
|
1426
|
+
const report = await status();
|
|
1427
|
+
process.stdout.write(`${JSON.stringify(report)}
|
|
1428
|
+
`);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1431
|
+
process.stderr.write(`${err.message}
|
|
1432
|
+
`);
|
|
1433
|
+
process.exitCode = 4;
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
throw err;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1380
1439
|
async function handleStatus(options, org) {
|
|
1440
|
+
if (options.json) {
|
|
1441
|
+
let backend2;
|
|
1442
|
+
let value2;
|
|
1443
|
+
try {
|
|
1444
|
+
backend2 = probeBackend();
|
|
1445
|
+
value2 = await getPat(org);
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1448
|
+
process.stderr.write(`${err.message}
|
|
1449
|
+
`);
|
|
1450
|
+
process.exitCode = 4;
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
throw err;
|
|
1454
|
+
}
|
|
1455
|
+
const storedEvents2 = readAuditEvents().filter((ev) => ev.org === org && ev.event === "auth.store");
|
|
1456
|
+
const last2 = storedEvents2.at(-1);
|
|
1457
|
+
const updatedAt2 = last2?.ts ?? null;
|
|
1458
|
+
if (!value2) {
|
|
1459
|
+
process.stdout.write(
|
|
1460
|
+
`${JSON.stringify({ org, backend: backend2, stored: false, masked: null, updated_at: updatedAt2 })}
|
|
1461
|
+
`
|
|
1462
|
+
);
|
|
1463
|
+
process.exitCode = 1;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const masked2 = maskedDisplay(value2);
|
|
1467
|
+
process.stdout.write(
|
|
1468
|
+
`${JSON.stringify({ org, backend: backend2, stored: true, masked: masked2, updated_at: updatedAt2 })}
|
|
1469
|
+
`
|
|
1470
|
+
);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1381
1473
|
let backend;
|
|
1382
1474
|
let value;
|
|
1383
1475
|
try {
|
|
@@ -1393,39 +1485,25 @@ async function handleStatus(options, org) {
|
|
|
1393
1485
|
throw err;
|
|
1394
1486
|
}
|
|
1395
1487
|
const storedEvents = readAuditEvents().filter((ev) => ev.org === org && ev.event === "auth.store");
|
|
1396
|
-
const last = storedEvents
|
|
1488
|
+
const last = storedEvents.at(-1);
|
|
1397
1489
|
const updatedAt = last?.ts ?? null;
|
|
1398
1490
|
if (!value) {
|
|
1399
|
-
|
|
1400
|
-
process.stdout.write(
|
|
1401
|
-
`${JSON.stringify({ org, backend, stored: false, masked: null, updated_at: updatedAt })}
|
|
1402
|
-
`
|
|
1403
|
-
);
|
|
1404
|
-
} else {
|
|
1405
|
-
process.stdout.write(`Organization: ${org}
|
|
1491
|
+
process.stdout.write(`Organization: ${org}
|
|
1406
1492
|
Backend: ${backend}
|
|
1407
1493
|
Stored: no
|
|
1408
1494
|
`);
|
|
1409
|
-
}
|
|
1410
1495
|
process.exitCode = 1;
|
|
1411
1496
|
return;
|
|
1412
1497
|
}
|
|
1413
1498
|
const masked = maskedDisplay(value);
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
`${JSON.stringify({ org, backend, stored: true, masked, updated_at: updatedAt })}
|
|
1417
|
-
`
|
|
1418
|
-
);
|
|
1419
|
-
} else {
|
|
1420
|
-
process.stdout.write(
|
|
1421
|
-
`Organization: ${org}
|
|
1499
|
+
process.stdout.write(
|
|
1500
|
+
`Organization: ${org}
|
|
1422
1501
|
Backend: ${backend}
|
|
1423
1502
|
Stored: yes
|
|
1424
1503
|
Identifier: ${masked}
|
|
1425
1504
|
` + (updatedAt ? `Last updated: ${updatedAt}
|
|
1426
1505
|
` : "")
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1506
|
+
);
|
|
1429
1507
|
}
|
|
1430
1508
|
async function handleLogout(options, orgFromGlobal) {
|
|
1431
1509
|
if (options.all && orgFromGlobal) {
|
|
@@ -1434,9 +1512,16 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1434
1512
|
return;
|
|
1435
1513
|
}
|
|
1436
1514
|
if (options.all) {
|
|
1437
|
-
let orgs;
|
|
1438
1515
|
try {
|
|
1439
|
-
|
|
1516
|
+
const result = await logout({ all: true });
|
|
1517
|
+
if (result.removed.length === 0) {
|
|
1518
|
+
process.stdout.write("No stored credentials to remove.\n");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
for (const r of result.removed) {
|
|
1522
|
+
process.stdout.write(`Removed ${r.kind} credential for org ${r.org}.
|
|
1523
|
+
`);
|
|
1524
|
+
}
|
|
1440
1525
|
} catch (err) {
|
|
1441
1526
|
if (err instanceof CredentialStoreUnavailableError) {
|
|
1442
1527
|
process.stderr.write(`${err.message}
|
|
@@ -1444,22 +1529,9 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1444
1529
|
process.exitCode = 4;
|
|
1445
1530
|
return;
|
|
1446
1531
|
}
|
|
1447
|
-
|
|
1448
|
-
}
|
|
1449
|
-
if (orgs.length === 0) {
|
|
1450
|
-
process.stdout.write("No stored PATs to remove.\n");
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
for (const org of orgs) {
|
|
1454
|
-
try {
|
|
1455
|
-
await deletePat(org);
|
|
1456
|
-
process.stdout.write(`PAT removed for org ${org}.
|
|
1457
|
-
`);
|
|
1458
|
-
} catch (err) {
|
|
1459
|
-
process.stderr.write(`Failed to remove PAT for org ${org}: ${err.message}
|
|
1532
|
+
process.stderr.write(`Failed to remove credentials: ${err.message}
|
|
1460
1533
|
`);
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1534
|
+
process.exitCode = 1;
|
|
1463
1535
|
}
|
|
1464
1536
|
return;
|
|
1465
1537
|
}
|
|
@@ -1473,10 +1545,10 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1473
1545
|
try {
|
|
1474
1546
|
const removed = await deletePat(resolved.org);
|
|
1475
1547
|
if (removed) {
|
|
1476
|
-
process.stdout.write(`
|
|
1548
|
+
process.stdout.write(`Credential removed for org ${resolved.org}.
|
|
1477
1549
|
`);
|
|
1478
1550
|
} else {
|
|
1479
|
-
process.stdout.write(`No stored
|
|
1551
|
+
process.stdout.write(`No stored credential for org ${resolved.org}.
|
|
1480
1552
|
`);
|
|
1481
1553
|
}
|
|
1482
1554
|
} catch (err) {
|
|
@@ -1491,14 +1563,70 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1491
1563
|
}
|
|
1492
1564
|
function createAuthCommand() {
|
|
1493
1565
|
const command = new Command3("auth");
|
|
1494
|
-
command.description(
|
|
1495
|
-
|
|
1496
|
-
|
|
1566
|
+
command.description(
|
|
1567
|
+
"Manage Azure DevOps authentication. Use `azdo auth login` for OAuth (default); the bare `azdo auth` form preserves the legacy PAT-prompt path for back-compat."
|
|
1568
|
+
);
|
|
1569
|
+
command.option("--org <name>", "Azure DevOps organization (flag wins over auto-detect / config)").option("--from-stdin", "read PAT from stdin instead of prompting (implies --use-pat)", false).option("--no-browser", "do not open the Azure DevOps PAT page in a browser (PAT path only)").option("--use-pat", "use Personal Access Token instead of OAuth (legacy path)", false).option("--device-code", "use OAuth device-code flow (headless hosts; OAuth only)", false).option("--client-id <id>", "override the default OAuth client id (FR-013 override path)").option("--tenant-id <id>", "override the default OAuth tenant id (default: common)").option("--scopes <scopes>", "space-separated OAuth scope override (advanced; default mirrors PAT scope table)");
|
|
1570
|
+
command.addHelpText(
|
|
1571
|
+
"after",
|
|
1572
|
+
`
|
|
1573
|
+
Default flow (OAuth, browser-based):
|
|
1574
|
+
azdo auth login --org <name>
|
|
1575
|
+
\u2192 opens the default browser for OAuth (Microsoft Entra v2 + PKCE).
|
|
1576
|
+
|
|
1577
|
+
Headless / no-browser:
|
|
1578
|
+
azdo auth login --org <name> --device-code
|
|
1579
|
+
|
|
1580
|
+
PAT path (legacy, opt-in):
|
|
1581
|
+
azdo auth login --org <name> --use-pat
|
|
1582
|
+
azdo auth --org <name> # back-compat alias of the above
|
|
1583
|
+
|
|
1584
|
+
OAuth scope set \u2014 shipped first-party client (default install):
|
|
1585
|
+
${firstPartyShippedScopes().join("\n ")}
|
|
1586
|
+
(uses ${AZDO_RESOURCE_ID}/.default \u2014 per-scope consent is unavailable
|
|
1587
|
+
against a client we do not own; .default grants the VS client's
|
|
1588
|
+
pre-authorized AzDO permissions in one step.)
|
|
1589
|
+
|
|
1590
|
+
OAuth scope set \u2014 self-registered apps (--client-id / AZDO_OAUTH_CLIENT_ID):
|
|
1591
|
+
${defaultScopes().join("\n ")}
|
|
1592
|
+
(FR-016, mirrors the PAT scope table \u2014 see docs/oauth-app-registration.md)
|
|
1593
|
+
|
|
1594
|
+
For self-registered OAuth apps (locked-down tenants), see docs/oauth-app-registration.md
|
|
1595
|
+
\u2014 that same guide is the maintainer reference for the project's shared client id.
|
|
1596
|
+
|
|
1597
|
+
Note: stored credentials may coexist as 'pat' or 'oauth' across orgs (FR-007).
|
|
1598
|
+
Note: \`azdo auth\` (no subcommand) preserves the legacy PAT-prompt entry point;
|
|
1599
|
+
\`azdo auth login\` is the spec-canonical name and defaults to OAuth.`
|
|
1600
|
+
).action(async (options) => {
|
|
1497
1601
|
await handleAuthRoot(options);
|
|
1498
1602
|
});
|
|
1499
|
-
const
|
|
1603
|
+
const loginCmd = command.command("login").description("Authenticate against Azure DevOps (OAuth default; --use-pat for PAT)").option("--org <name>", "Azure DevOps organization (defaults: git remote \u2192 config)");
|
|
1604
|
+
loginCmd.action(async () => {
|
|
1605
|
+
const merged = loginCmd.optsWithGlobals();
|
|
1606
|
+
await handleLoginSubcommand(merged);
|
|
1607
|
+
});
|
|
1608
|
+
const statusCmd = command.command("status").description("Report stored credentials (kind, org, account/expiry, backend) \u2014 never the token").option("--json", "emit JSON", false);
|
|
1500
1609
|
statusCmd.action(async (options) => {
|
|
1501
1610
|
const globals = statusCmd.optsWithGlobals();
|
|
1611
|
+
if (!globals.org) {
|
|
1612
|
+
if (options.json) {
|
|
1613
|
+
await handleStatusJson();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const report = await status();
|
|
1617
|
+
if (report.orgs.length === 0) {
|
|
1618
|
+
process.stdout.write("No stored credentials.\n");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
for (const e of report.orgs) {
|
|
1622
|
+
const expiry = e.expiresAt ? new Date(e.expiresAt * 1e3).toISOString() : "n/a";
|
|
1623
|
+
process.stdout.write(
|
|
1624
|
+
`${e.org} ${e.kind} ${e.accountId ?? ""} ${expiry}
|
|
1625
|
+
`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1502
1630
|
const resolved = resolveOrg({ org: globals.org });
|
|
1503
1631
|
if (!resolved) {
|
|
1504
1632
|
process.stderr.write(`${formatResolutionError()}
|
|
@@ -1508,7 +1636,11 @@ function createAuthCommand() {
|
|
|
1508
1636
|
}
|
|
1509
1637
|
await handleStatus(options, resolved.org);
|
|
1510
1638
|
});
|
|
1511
|
-
|
|
1639
|
+
statusCmd.addHelpText(
|
|
1640
|
+
"after",
|
|
1641
|
+
"\nStored credentials may be of kind `pat` or `oauth` and may coexist across orgs (FR-007).\n"
|
|
1642
|
+
);
|
|
1643
|
+
const logoutCmd = command.command("logout").description("Remove the stored credential for an org (or all orgs with --all)").option("--all", "remove every stored credential (PAT and OAuth)", false);
|
|
1512
1644
|
logoutCmd.action(async (options) => {
|
|
1513
1645
|
const globals = logoutCmd.optsWithGlobals();
|
|
1514
1646
|
await handleLogout(options, globals.org);
|
|
@@ -1690,11 +1822,11 @@ function createSetStateCommand() {
|
|
|
1690
1822
|
let context;
|
|
1691
1823
|
try {
|
|
1692
1824
|
context = resolveContext(options);
|
|
1693
|
-
const credential = await
|
|
1825
|
+
const credential = await requireAuthCredential(context.org);
|
|
1694
1826
|
const operations = [
|
|
1695
1827
|
{ op: "add", path: "/fields/System.State", value: state }
|
|
1696
1828
|
];
|
|
1697
|
-
const result = await updateWorkItem(context, id, credential
|
|
1829
|
+
const result = await updateWorkItem(context, id, credential, "System.State", operations);
|
|
1698
1830
|
if (options.json) {
|
|
1699
1831
|
process.stdout.write(
|
|
1700
1832
|
JSON.stringify({
|
|
@@ -1740,12 +1872,12 @@ function createAssignCommand() {
|
|
|
1740
1872
|
let context;
|
|
1741
1873
|
try {
|
|
1742
1874
|
context = resolveContext(options);
|
|
1743
|
-
const credential = await
|
|
1875
|
+
const credential = await requireAuthCredential(context.org);
|
|
1744
1876
|
const value = options.unassign ? "" : name;
|
|
1745
1877
|
const operations = [
|
|
1746
1878
|
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
1747
1879
|
];
|
|
1748
|
-
const result = await updateWorkItem(context, id, credential
|
|
1880
|
+
const result = await updateWorkItem(context, id, credential, "System.AssignedTo", operations);
|
|
1749
1881
|
if (options.json) {
|
|
1750
1882
|
process.stdout.write(
|
|
1751
1883
|
JSON.stringify({
|
|
@@ -1780,11 +1912,11 @@ function createSetFieldCommand() {
|
|
|
1780
1912
|
let context;
|
|
1781
1913
|
try {
|
|
1782
1914
|
context = resolveContext(options);
|
|
1783
|
-
const credential = await
|
|
1915
|
+
const credential = await requireAuthCredential(context.org);
|
|
1784
1916
|
const operations = [
|
|
1785
1917
|
{ op: "add", path: `/fields/${field}`, value }
|
|
1786
1918
|
];
|
|
1787
|
-
const result = await updateWorkItem(context, id, credential
|
|
1919
|
+
const result = await updateWorkItem(context, id, credential, field, operations);
|
|
1788
1920
|
if (options.json) {
|
|
1789
1921
|
process.stdout.write(
|
|
1790
1922
|
JSON.stringify({
|
|
@@ -1818,8 +1950,8 @@ function createGetMdFieldCommand() {
|
|
|
1818
1950
|
let context;
|
|
1819
1951
|
try {
|
|
1820
1952
|
context = resolveContext(options);
|
|
1821
|
-
const credential = await
|
|
1822
|
-
const value = await getWorkItemFieldValue(context, id, credential
|
|
1953
|
+
const credential = await requireAuthCredential(context.org);
|
|
1954
|
+
const value = await getWorkItemFieldValue(context, id, credential, field);
|
|
1823
1955
|
if (value === null) {
|
|
1824
1956
|
process.stdout.write("\n");
|
|
1825
1957
|
} else {
|
|
@@ -1906,12 +2038,12 @@ function createSetMdFieldCommand() {
|
|
|
1906
2038
|
let context;
|
|
1907
2039
|
try {
|
|
1908
2040
|
context = resolveContext(options);
|
|
1909
|
-
const credential = await
|
|
2041
|
+
const credential = await requireAuthCredential(context.org);
|
|
1910
2042
|
const operations = [
|
|
1911
2043
|
{ op: "add", path: `/fields/${field}`, value: content },
|
|
1912
2044
|
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
1913
2045
|
];
|
|
1914
|
-
const result = await updateWorkItem(context, id, credential
|
|
2046
|
+
const result = await updateWorkItem(context, id, credential, field, operations);
|
|
1915
2047
|
formatOutput(result, options, field);
|
|
1916
2048
|
} catch (err) {
|
|
1917
2049
|
handleCommandError(err, id, context, "write");
|
|
@@ -2222,15 +2354,15 @@ function createUpsertCommand() {
|
|
|
2222
2354
|
ensureTitleForCreate(document.fields);
|
|
2223
2355
|
}
|
|
2224
2356
|
const operations = toPatchOperations(document.fields, action);
|
|
2225
|
-
const credential = await
|
|
2357
|
+
const credential = await requireAuthCredential(context.org);
|
|
2226
2358
|
let writeResult;
|
|
2227
2359
|
if (action === "created") {
|
|
2228
|
-
writeResult = await createWorkItem(context, createType, credential
|
|
2360
|
+
writeResult = await createWorkItem(context, createType, credential, operations);
|
|
2229
2361
|
} else {
|
|
2230
2362
|
if (id === void 0) {
|
|
2231
2363
|
fail2("Work item ID is required for updates.");
|
|
2232
2364
|
}
|
|
2233
|
-
writeResult = await applyWorkItemPatch(context, id, credential
|
|
2365
|
+
writeResult = await applyWorkItemPatch(context, id, credential, operations);
|
|
2234
2366
|
}
|
|
2235
2367
|
const result = buildUpsertResult(
|
|
2236
2368
|
action,
|
|
@@ -2288,8 +2420,8 @@ function createListFieldsCommand() {
|
|
|
2288
2420
|
let context;
|
|
2289
2421
|
try {
|
|
2290
2422
|
context = resolveContext(options);
|
|
2291
|
-
const credential = await
|
|
2292
|
-
const fields = await getWorkItemFields(context, id, credential
|
|
2423
|
+
const credential = await requireAuthCredential(context.org);
|
|
2424
|
+
const fields = await getWorkItemFields(context, id, credential);
|
|
2293
2425
|
if (options.json) {
|
|
2294
2426
|
process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
|
|
2295
2427
|
} else {
|
|
@@ -2343,9 +2475,9 @@ function mapPullRequest(repo, pullRequest) {
|
|
|
2343
2475
|
url: pullRequest._links?.web?.href ?? null
|
|
2344
2476
|
};
|
|
2345
2477
|
}
|
|
2346
|
-
function mapPullRequestCheckName(
|
|
2347
|
-
const genre =
|
|
2348
|
-
const name =
|
|
2478
|
+
function mapPullRequestCheckName(status2) {
|
|
2479
|
+
const genre = status2.context?.genre?.trim();
|
|
2480
|
+
const name = status2.context?.name?.trim();
|
|
2349
2481
|
if (genre && name) {
|
|
2350
2482
|
return `${genre}/${name}`;
|
|
2351
2483
|
}
|
|
@@ -2355,21 +2487,21 @@ function mapPullRequestCheckName(status) {
|
|
|
2355
2487
|
if (genre) {
|
|
2356
2488
|
return genre;
|
|
2357
2489
|
}
|
|
2358
|
-
return `Status #${
|
|
2490
|
+
return `Status #${status2.id}`;
|
|
2359
2491
|
}
|
|
2360
|
-
function mapPullRequestCheck(
|
|
2361
|
-
if (
|
|
2492
|
+
function mapPullRequestCheck(status2) {
|
|
2493
|
+
if (status2.state === "notApplicable" || status2.state === "notSet") {
|
|
2362
2494
|
return null;
|
|
2363
2495
|
}
|
|
2364
2496
|
return {
|
|
2365
|
-
id:
|
|
2366
|
-
state:
|
|
2367
|
-
name: mapPullRequestCheckName(
|
|
2368
|
-
description:
|
|
2369
|
-
targetUrl:
|
|
2370
|
-
createdBy:
|
|
2371
|
-
createdAt:
|
|
2372
|
-
updatedAt:
|
|
2497
|
+
id: status2.id,
|
|
2498
|
+
state: status2.state,
|
|
2499
|
+
name: mapPullRequestCheckName(status2),
|
|
2500
|
+
description: status2.description ?? null,
|
|
2501
|
+
targetUrl: status2.targetUrl ?? null,
|
|
2502
|
+
createdBy: status2.createdBy?.displayName ?? null,
|
|
2503
|
+
createdAt: status2.creationDate ?? null,
|
|
2504
|
+
updatedAt: status2.updatedDate ?? null
|
|
2373
2505
|
};
|
|
2374
2506
|
}
|
|
2375
2507
|
function mapComment(comment) {
|
|
@@ -2405,8 +2537,8 @@ function toActiveCommentThread(thread) {
|
|
|
2405
2537
|
};
|
|
2406
2538
|
}
|
|
2407
2539
|
var RESOLVED_THREAD_STATUSES = /* @__PURE__ */ new Set(["fixed", "wontFix", "closed", "byDesign"]);
|
|
2408
|
-
function isThreadResolved(
|
|
2409
|
-
return RESOLVED_THREAD_STATUSES.has(
|
|
2540
|
+
function isThreadResolved(status2) {
|
|
2541
|
+
return RESOLVED_THREAD_STATUSES.has(status2);
|
|
2410
2542
|
}
|
|
2411
2543
|
async function readJsonResponse(response) {
|
|
2412
2544
|
if (!response.ok) {
|
|
@@ -2414,7 +2546,7 @@ async function readJsonResponse(response) {
|
|
|
2414
2546
|
}
|
|
2415
2547
|
return response.json();
|
|
2416
2548
|
}
|
|
2417
|
-
async function patchThreadStatus(context, repo,
|
|
2549
|
+
async function patchThreadStatus(context, repo, cred, prId, threadId, status2) {
|
|
2418
2550
|
const url = new URL(
|
|
2419
2551
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}`
|
|
2420
2552
|
);
|
|
@@ -2422,41 +2554,41 @@ async function patchThreadStatus(context, repo, pat, prId, threadId, status) {
|
|
|
2422
2554
|
const response = await fetchWithErrors(url.toString(), {
|
|
2423
2555
|
method: "PATCH",
|
|
2424
2556
|
headers: {
|
|
2425
|
-
...authHeaders(
|
|
2557
|
+
...authHeaders(cred),
|
|
2426
2558
|
"Content-Type": "application/json"
|
|
2427
2559
|
},
|
|
2428
|
-
body: JSON.stringify({ status })
|
|
2560
|
+
body: JSON.stringify({ status: status2 })
|
|
2429
2561
|
});
|
|
2430
2562
|
const data = await readJsonResponse(response);
|
|
2431
2563
|
return toActiveCommentThread(data);
|
|
2432
2564
|
}
|
|
2433
|
-
async function getPullRequestById(context, repo,
|
|
2565
|
+
async function getPullRequestById(context, repo, cred, prId) {
|
|
2434
2566
|
const url = new URL(
|
|
2435
2567
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}`
|
|
2436
2568
|
);
|
|
2437
2569
|
url.searchParams.set("api-version", "7.1");
|
|
2438
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
2570
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
2439
2571
|
const data = await readJsonResponse(response);
|
|
2440
2572
|
return mapPullRequest(repo, data);
|
|
2441
2573
|
}
|
|
2442
|
-
async function listPullRequests(context, repo,
|
|
2574
|
+
async function listPullRequests(context, repo, cred, sourceBranch, opts) {
|
|
2443
2575
|
const response = await fetchWithErrors(
|
|
2444
2576
|
buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
|
|
2445
|
-
{ headers: authHeaders(
|
|
2577
|
+
{ headers: authHeaders(cred) }
|
|
2446
2578
|
);
|
|
2447
2579
|
const data = await readJsonResponse(response);
|
|
2448
2580
|
return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
|
|
2449
2581
|
}
|
|
2450
|
-
async function getPullRequestChecks(context, repo,
|
|
2582
|
+
async function getPullRequestChecks(context, repo, cred, prId) {
|
|
2451
2583
|
const response = await fetchWithErrors(
|
|
2452
2584
|
buildPullRequestStatusesUrl(context, repo, prId).toString(),
|
|
2453
|
-
{ headers: authHeaders(
|
|
2585
|
+
{ headers: authHeaders(cred) }
|
|
2454
2586
|
);
|
|
2455
2587
|
const data = await readJsonResponse(response);
|
|
2456
2588
|
return data.value.map(mapPullRequestCheck).filter((check) => check !== null);
|
|
2457
2589
|
}
|
|
2458
|
-
async function openPullRequest(context, repo,
|
|
2459
|
-
const existing = await listPullRequests(context, repo,
|
|
2590
|
+
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
2591
|
+
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
2460
2592
|
status: "active",
|
|
2461
2593
|
targetBranch: "develop"
|
|
2462
2594
|
});
|
|
@@ -2484,7 +2616,7 @@ async function openPullRequest(context, repo, pat, sourceBranch, title, descript
|
|
|
2484
2616
|
const response = await fetchWithErrors(url.toString(), {
|
|
2485
2617
|
method: "POST",
|
|
2486
2618
|
headers: {
|
|
2487
|
-
...authHeaders(
|
|
2619
|
+
...authHeaders(cred),
|
|
2488
2620
|
"Content-Type": "application/json"
|
|
2489
2621
|
},
|
|
2490
2622
|
body: JSON.stringify(payload)
|
|
@@ -2497,12 +2629,12 @@ async function openPullRequest(context, repo, pat, sourceBranch, title, descript
|
|
|
2497
2629
|
pullRequest: mapPullRequest(repo, data)
|
|
2498
2630
|
};
|
|
2499
2631
|
}
|
|
2500
|
-
async function getPullRequestThreads(context, repo,
|
|
2632
|
+
async function getPullRequestThreads(context, repo, cred, prId) {
|
|
2501
2633
|
const url = new URL(
|
|
2502
2634
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
2503
2635
|
);
|
|
2504
2636
|
url.searchParams.set("api-version", "7.1");
|
|
2505
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
2637
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
2506
2638
|
const data = await readJsonResponse(response);
|
|
2507
2639
|
return data.value.map(mapThread).filter((thread) => thread !== null);
|
|
2508
2640
|
}
|
|
@@ -2515,6 +2647,21 @@ function parsePositivePrNumber(raw) {
|
|
|
2515
2647
|
const n = Number.parseInt(raw, 10);
|
|
2516
2648
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
2517
2649
|
}
|
|
2650
|
+
var PR_NUMBER_HELP = "target the pull request with this numeric id, instead of the current branch's PR. When omitted, the CLI auto-detects the pull request whose source branch equals refs/heads/<current branch> in the Azure DevOps repository identified by the origin remote; if zero or more than one open PR matches, the command fails with a message naming the searched branch.";
|
|
2651
|
+
function configureUnwrappedHelp(command) {
|
|
2652
|
+
return command.configureHelp({ helpWidth: 1e3 });
|
|
2653
|
+
}
|
|
2654
|
+
function autoDetectZeroMatch(branch) {
|
|
2655
|
+
return `No open pull request matches branch ${branch}. Pass --pr-number to target a specific PR, or push the branch and open a pull request.`;
|
|
2656
|
+
}
|
|
2657
|
+
function autoDetectMultiMatch(branch, ids) {
|
|
2658
|
+
return `Multiple open pull requests match branch ${branch}: ${ids.map((id) => `#${id}`).join(", ")}. Re-run with --pr-number to choose.`;
|
|
2659
|
+
}
|
|
2660
|
+
function writeContractError(line) {
|
|
2661
|
+
process.stderr.write(`${line}
|
|
2662
|
+
`);
|
|
2663
|
+
process.exitCode = 1;
|
|
2664
|
+
}
|
|
2518
2665
|
function formatBranchName(refName) {
|
|
2519
2666
|
return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
|
|
2520
2667
|
}
|
|
@@ -2569,8 +2716,8 @@ function formatPullRequestBlock(pullRequest) {
|
|
|
2569
2716
|
...formatPullRequestChecks(pullRequest.checks)
|
|
2570
2717
|
].join("\n");
|
|
2571
2718
|
}
|
|
2572
|
-
function threadStatusLabel(
|
|
2573
|
-
return isThreadResolved(
|
|
2719
|
+
function threadStatusLabel(status2) {
|
|
2720
|
+
return isThreadResolved(status2) ? "resolved" : status2;
|
|
2574
2721
|
}
|
|
2575
2722
|
function formatThreads(prId, title, threads) {
|
|
2576
2723
|
const lines = [`Comment threads for pull request #${prId}: ${title}`];
|
|
@@ -2587,12 +2734,12 @@ async function resolvePrCommandContext(options, resolveOpts = {}) {
|
|
|
2587
2734
|
const context = resolveContext(options);
|
|
2588
2735
|
const repo = detectRepoName();
|
|
2589
2736
|
const branch = requireBranch ? getCurrentBranch() : null;
|
|
2590
|
-
const credential = await
|
|
2737
|
+
const credential = await requireAuthCredential(context.org);
|
|
2591
2738
|
return {
|
|
2592
2739
|
context,
|
|
2593
2740
|
repo,
|
|
2594
2741
|
branch,
|
|
2595
|
-
pat: credential
|
|
2742
|
+
pat: credential
|
|
2596
2743
|
};
|
|
2597
2744
|
}
|
|
2598
2745
|
function createPrStatusCommand() {
|
|
@@ -2690,7 +2837,7 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
2690
2837
|
}
|
|
2691
2838
|
function createPrCommentsCommand() {
|
|
2692
2839
|
const command = new Command12("comments");
|
|
2693
|
-
command.description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>",
|
|
2840
|
+
configureUnwrappedHelp(command).description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
|
|
2694
2841
|
validateOrgProjectPair(options);
|
|
2695
2842
|
let context;
|
|
2696
2843
|
let explicitPrId = null;
|
|
@@ -2722,12 +2869,11 @@ function createPrCommentsCommand() {
|
|
|
2722
2869
|
status: "active"
|
|
2723
2870
|
});
|
|
2724
2871
|
if (pullRequests.length === 0) {
|
|
2725
|
-
|
|
2872
|
+
writeContractError(autoDetectZeroMatch(resolved.branch));
|
|
2726
2873
|
return;
|
|
2727
2874
|
}
|
|
2728
2875
|
if (pullRequests.length > 1) {
|
|
2729
|
-
|
|
2730
|
-
writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
|
|
2876
|
+
writeContractError(autoDetectMultiMatch(resolved.branch, pullRequests.map((pr) => pr.id)));
|
|
2731
2877
|
return;
|
|
2732
2878
|
}
|
|
2733
2879
|
pullRequest = pullRequests[0];
|
|
@@ -2791,12 +2937,11 @@ async function resolveThreadTarget(threadIdRaw, options) {
|
|
|
2791
2937
|
status: "active"
|
|
2792
2938
|
});
|
|
2793
2939
|
if (pullRequests.length === 0) {
|
|
2794
|
-
|
|
2940
|
+
writeContractError(autoDetectZeroMatch(resolved.branch));
|
|
2795
2941
|
return null;
|
|
2796
2942
|
}
|
|
2797
2943
|
if (pullRequests.length > 1) {
|
|
2798
|
-
|
|
2799
|
-
writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
|
|
2944
|
+
writeContractError(autoDetectMultiMatch(resolved.branch, pullRequests.map((pr) => pr.id)));
|
|
2800
2945
|
return null;
|
|
2801
2946
|
}
|
|
2802
2947
|
pullRequest = pullRequests[0];
|
|
@@ -2864,14 +3009,14 @@ async function runThreadStateChange(threadIdRaw, options, direction) {
|
|
|
2864
3009
|
}
|
|
2865
3010
|
function createPrCommentResolveCommand() {
|
|
2866
3011
|
const command = new Command12("comment-resolve");
|
|
2867
|
-
command.description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>",
|
|
3012
|
+
configureUnwrappedHelp(command).description("Mark a pull request comment thread as resolved").argument("<threadId>", "numeric id of the thread to resolve").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
|
|
2868
3013
|
await runThreadStateChange(threadIdRaw, options, "resolve");
|
|
2869
3014
|
});
|
|
2870
3015
|
return command;
|
|
2871
3016
|
}
|
|
2872
3017
|
function createPrCommentReopenCommand() {
|
|
2873
3018
|
const command = new Command12("comment-reopen");
|
|
2874
|
-
command.description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>",
|
|
3019
|
+
configureUnwrappedHelp(command).description("Reopen (set to active) a previously resolved pull request comment thread").argument("<threadId>", "numeric id of the thread to reopen").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, options) => {
|
|
2875
3020
|
await runThreadStateChange(threadIdRaw, options, "reopen");
|
|
2876
3021
|
});
|
|
2877
3022
|
return command;
|
|
@@ -2915,8 +3060,8 @@ function createCommentsListCommand() {
|
|
|
2915
3060
|
let context;
|
|
2916
3061
|
try {
|
|
2917
3062
|
context = resolveContext(options);
|
|
2918
|
-
const credential = await
|
|
2919
|
-
const result = await listWorkItemComments(context, id, credential
|
|
3063
|
+
const credential = await requireAuthCredential(context.org);
|
|
3064
|
+
const result = await listWorkItemComments(context, id, credential);
|
|
2920
3065
|
if (options.json) {
|
|
2921
3066
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2922
3067
|
`);
|
|
@@ -2946,9 +3091,9 @@ function createCommentsAddCommand() {
|
|
|
2946
3091
|
let context;
|
|
2947
3092
|
try {
|
|
2948
3093
|
context = resolveContext(options);
|
|
2949
|
-
const credential = await
|
|
3094
|
+
const credential = await requireAuthCredential(context.org);
|
|
2950
3095
|
const format = options.markdown === true ? "markdown" : "html";
|
|
2951
|
-
const result = await addWorkItemComment(context, id, credential
|
|
3096
|
+
const result = await addWorkItemComment(context, id, credential, text, format);
|
|
2952
3097
|
if (options.json) {
|
|
2953
3098
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2954
3099
|
`);
|
|
@@ -2984,14 +3129,14 @@ function createDownloadAttachmentCommand() {
|
|
|
2984
3129
|
let context;
|
|
2985
3130
|
try {
|
|
2986
3131
|
context = resolveContext(options);
|
|
2987
|
-
const credential = await
|
|
3132
|
+
const credential = await requireAuthCredential(context.org);
|
|
2988
3133
|
const outputDir = options.output ?? ".";
|
|
2989
3134
|
if (!existsSync4(outputDir)) {
|
|
2990
3135
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
2991
3136
|
`);
|
|
2992
3137
|
process.exit(1);
|
|
2993
3138
|
}
|
|
2994
|
-
const workItem = await getWorkItem(context, id, credential
|
|
3139
|
+
const workItem = await getWorkItem(context, id, credential);
|
|
2995
3140
|
const attachment = workItem.attachments?.find(
|
|
2996
3141
|
(a) => a.name === filename
|
|
2997
3142
|
);
|
|
@@ -3002,7 +3147,7 @@ function createDownloadAttachmentCommand() {
|
|
|
3002
3147
|
);
|
|
3003
3148
|
process.exit(1);
|
|
3004
3149
|
}
|
|
3005
|
-
const data = await downloadAttachment(attachment.url, credential
|
|
3150
|
+
const data = await downloadAttachment(attachment.url, credential);
|
|
3006
3151
|
const outputPath = join2(outputDir, filename);
|
|
3007
3152
|
await writeFile(outputPath, Buffer.from(data));
|
|
3008
3153
|
process.stdout.write(
|