azdo-cli 0.10.0-develop.264 → 0.10.0-develop.317
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/dist/chunk-C7RAZJHV.js +1087 -0
- package/dist/index.js +603 -494
- 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;
|
|
470
|
+
if (errCode === "access_denied") {
|
|
471
|
+
throw new DeviceCodeFlowError("access_denied", "authorisation denied by user");
|
|
549
472
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
var legacyUnsetNoticeEmitted = false;
|
|
584
|
-
function emitLegacyUnsetNoticeOnce() {
|
|
585
|
-
if (legacyUnsetNoticeEmitted) return;
|
|
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
504
|
}
|
|
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
505
|
}
|
|
632
|
-
async function
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
const migrated = await maybeMigrateLegacy(org);
|
|
639
|
-
return migrated;
|
|
640
|
-
}
|
|
641
|
-
async function storePat(org, pat) {
|
|
642
|
-
const entry = new Entry(SERVICE, accountFor(org));
|
|
643
|
-
wrapUnavailable(() => entry.setPassword(pat));
|
|
644
|
-
appendAuthAuditEvent({
|
|
645
|
-
event: "auth.store",
|
|
646
|
-
org,
|
|
647
|
-
backend: probeBackend(),
|
|
648
|
-
masked_pat: maskedDisplay(pat)
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
async function deletePat(org) {
|
|
652
|
-
const entry = new Entry(SERVICE, accountFor(org));
|
|
653
|
-
const existing = wrapUnavailable(() => entry.getPassword());
|
|
654
|
-
if (existing === null) {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
|
-
wrapUnavailable(() => entry.deletePassword());
|
|
658
|
-
appendAuthAuditEvent({
|
|
659
|
-
event: "auth.delete",
|
|
660
|
-
org,
|
|
661
|
-
backend: probeBackend(),
|
|
662
|
-
masked_pat: maskedDisplay(existing)
|
|
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);
|
|
663
511
|
});
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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,6 +640,129 @@ 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";
|
|
@@ -1073,12 +1045,12 @@ function stripHtml(html) {
|
|
|
1073
1045
|
text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
|
|
1074
1046
|
text = text.replaceAll(/<li>/gi, "\n");
|
|
1075
1047
|
text = removeHtmlTags(text);
|
|
1076
|
-
text = text.replaceAll("&", "&");
|
|
1077
1048
|
text = text.replaceAll("<", "<");
|
|
1078
1049
|
text = text.replaceAll(">", ">");
|
|
1079
1050
|
text = text.replaceAll(""", '"');
|
|
1080
1051
|
text = text.replaceAll("'", "'");
|
|
1081
1052
|
text = text.replaceAll(" ", " ");
|
|
1053
|
+
text = text.replaceAll("&", "&");
|
|
1082
1054
|
text = text.replaceAll(/\n{3,}/g, "\n\n");
|
|
1083
1055
|
return text.trim();
|
|
1084
1056
|
}
|
|
@@ -1188,9 +1160,9 @@ function createGetItemCommand() {
|
|
|
1188
1160
|
let context;
|
|
1189
1161
|
try {
|
|
1190
1162
|
context = resolveContext(options);
|
|
1191
|
-
const credential = await
|
|
1163
|
+
const credential = await requireAuthCredential(context.org);
|
|
1192
1164
|
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
1193
|
-
const workItem = await getWorkItem(context, id, credential
|
|
1165
|
+
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1194
1166
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
1195
1167
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1196
1168
|
process.stdout.write(output + "\n");
|
|
@@ -1229,63 +1201,6 @@ function createClearPatCommand() {
|
|
|
1229
1201
|
|
|
1230
1202
|
// src/commands/auth.ts
|
|
1231
1203
|
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
1204
|
async function readStdinToString() {
|
|
1290
1205
|
const chunks = [];
|
|
1291
1206
|
for await (const chunk of process.stdin) {
|
|
@@ -1293,9 +1208,9 @@ async function readStdinToString() {
|
|
|
1293
1208
|
}
|
|
1294
1209
|
return Buffer.concat(chunks).toString("utf8");
|
|
1295
1210
|
}
|
|
1296
|
-
async function
|
|
1211
|
+
async function promptYesNo(prompt) {
|
|
1297
1212
|
if (!process.stdin.isTTY) return true;
|
|
1298
|
-
process.stderr.write(
|
|
1213
|
+
process.stderr.write(prompt);
|
|
1299
1214
|
return await new Promise((resolve2) => {
|
|
1300
1215
|
process.stdin.setEncoding("utf8");
|
|
1301
1216
|
let answered = false;
|
|
@@ -1311,7 +1226,23 @@ async function confirmOverwrite(org) {
|
|
|
1311
1226
|
process.stdin.on("data", handler);
|
|
1312
1227
|
});
|
|
1313
1228
|
}
|
|
1314
|
-
async function
|
|
1229
|
+
async function confirmOverwrite(org) {
|
|
1230
|
+
return promptYesNo(`A PAT is already stored for org ${org}. Overwrite? [y/N] `);
|
|
1231
|
+
}
|
|
1232
|
+
async function confirmOverwriteCredential(org, existingKind) {
|
|
1233
|
+
const label = existingKind === "oauth" ? "OAuth credential" : "PAT";
|
|
1234
|
+
return promptYesNo(`A ${label} is already stored for org ${org}. The new login will replace it. Continue? [y/N] `);
|
|
1235
|
+
}
|
|
1236
|
+
function rejectMutuallyExclusive(opts) {
|
|
1237
|
+
if (opts.usePat && opts.deviceCode) {
|
|
1238
|
+
return "--use-pat and --device-code are mutually exclusive (PAT has no device-code flow).";
|
|
1239
|
+
}
|
|
1240
|
+
if (opts.usePat && (opts.clientId || opts.tenantId || opts.scopes)) {
|
|
1241
|
+
return "--use-pat cannot be combined with OAuth-only flags (--client-id / --tenant-id / --scopes).";
|
|
1242
|
+
}
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
async function handlePatLogin(options) {
|
|
1315
1246
|
const resolved = resolveOrg({ org: options.org });
|
|
1316
1247
|
if (!resolved) {
|
|
1317
1248
|
process.stderr.write(`${formatResolutionError()}
|
|
@@ -1377,7 +1308,145 @@ async function handleAuthRoot(options) {
|
|
|
1377
1308
|
process.stdout.write(`PAT stored for org ${org} in ${probeBackend()}.
|
|
1378
1309
|
`);
|
|
1379
1310
|
}
|
|
1311
|
+
async function ensureOverwriteConfirmed(org) {
|
|
1312
|
+
try {
|
|
1313
|
+
const existing = await getStoredCredential(org);
|
|
1314
|
+
if (existing === null || !process.stdin.isTTY) return "ok";
|
|
1315
|
+
const ok = await confirmOverwriteCredential(org, existing.kind);
|
|
1316
|
+
return ok ? "ok" : "aborted";
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1319
|
+
process.stderr.write(`${err.message}
|
|
1320
|
+
`);
|
|
1321
|
+
return "unavailable";
|
|
1322
|
+
}
|
|
1323
|
+
throw err;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function buildOAuthLoginOptions(options) {
|
|
1327
|
+
return {
|
|
1328
|
+
flow: options.deviceCode ? "device-code" : "auto",
|
|
1329
|
+
clientIdOverride: options.clientId,
|
|
1330
|
+
tenantIdOverride: options.tenantId,
|
|
1331
|
+
scopesOverride: options.scopes ? options.scopes.split(/\s+/).filter(Boolean) : void 0
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function reportOAuthFailure(err) {
|
|
1335
|
+
const reason = typeof err === "object" && err !== null && "reason" in err ? err.reason : null;
|
|
1336
|
+
const msg = err.message;
|
|
1337
|
+
process.stderr.write(reason ? `OAuth login failed (${reason}): ${msg}
|
|
1338
|
+
` : `OAuth login failed: ${msg}
|
|
1339
|
+
`);
|
|
1340
|
+
const noDisplay = process.platform === "linux" && (!process.env.DISPLAY || process.env.DISPLAY.length === 0);
|
|
1341
|
+
if (noDisplay) {
|
|
1342
|
+
process.stderr.write("Tip: this host has no DISPLAY; pass --device-code to use the headless flow.\n");
|
|
1343
|
+
} else if (reason === "port-conflict") {
|
|
1344
|
+
process.stderr.write("Tip: another process is using the loopback callback port. Try again or pass --device-code.\n");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async function handleOAuthLogin(options) {
|
|
1348
|
+
const resolved = resolveOrg({ org: options.org });
|
|
1349
|
+
if (!resolved) {
|
|
1350
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1351
|
+
`);
|
|
1352
|
+
process.exitCode = 3;
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const org = resolved.org;
|
|
1356
|
+
const confirm = await ensureOverwriteConfirmed(org);
|
|
1357
|
+
if (confirm === "aborted") {
|
|
1358
|
+
process.stderr.write("Aborted. Existing credential preserved.\n");
|
|
1359
|
+
process.exitCode = 1;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (confirm === "unavailable") {
|
|
1363
|
+
process.exitCode = 4;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
const result = await loginWithOAuth(org, buildOAuthLoginOptions(options));
|
|
1368
|
+
process.stdout.write(
|
|
1369
|
+
`Logged in to ${org} via OAuth (${result.flowUsed}). Account: ${result.accountId}; expires ${new Date(result.expiresAt * 1e3).toISOString()}.
|
|
1370
|
+
`
|
|
1371
|
+
);
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
reportOAuthFailure(err);
|
|
1374
|
+
process.exitCode = 1;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async function handleAuthRoot(options) {
|
|
1378
|
+
const conflict = rejectMutuallyExclusive(options);
|
|
1379
|
+
if (conflict) {
|
|
1380
|
+
process.stderr.write(`${conflict}
|
|
1381
|
+
`);
|
|
1382
|
+
process.exitCode = 2;
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
await handlePatLogin(options);
|
|
1386
|
+
}
|
|
1387
|
+
async function handleLoginSubcommand(options) {
|
|
1388
|
+
const conflict = rejectMutuallyExclusive(options);
|
|
1389
|
+
if (conflict) {
|
|
1390
|
+
process.stderr.write(`${conflict}
|
|
1391
|
+
`);
|
|
1392
|
+
process.exitCode = 2;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (options.fromStdin || options.usePat) {
|
|
1396
|
+
await handlePatLogin(options);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await handleOAuthLogin(options);
|
|
1400
|
+
}
|
|
1401
|
+
async function handleStatusJson() {
|
|
1402
|
+
try {
|
|
1403
|
+
const report = await status();
|
|
1404
|
+
process.stdout.write(`${JSON.stringify(report)}
|
|
1405
|
+
`);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1408
|
+
process.stderr.write(`${err.message}
|
|
1409
|
+
`);
|
|
1410
|
+
process.exitCode = 4;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
throw err;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1380
1416
|
async function handleStatus(options, org) {
|
|
1417
|
+
if (options.json) {
|
|
1418
|
+
let backend2;
|
|
1419
|
+
let value2;
|
|
1420
|
+
try {
|
|
1421
|
+
backend2 = probeBackend();
|
|
1422
|
+
value2 = await getPat(org);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1425
|
+
process.stderr.write(`${err.message}
|
|
1426
|
+
`);
|
|
1427
|
+
process.exitCode = 4;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
throw err;
|
|
1431
|
+
}
|
|
1432
|
+
const storedEvents2 = readAuditEvents().filter((ev) => ev.org === org && ev.event === "auth.store");
|
|
1433
|
+
const last2 = storedEvents2.at(-1);
|
|
1434
|
+
const updatedAt2 = last2?.ts ?? null;
|
|
1435
|
+
if (!value2) {
|
|
1436
|
+
process.stdout.write(
|
|
1437
|
+
`${JSON.stringify({ org, backend: backend2, stored: false, masked: null, updated_at: updatedAt2 })}
|
|
1438
|
+
`
|
|
1439
|
+
);
|
|
1440
|
+
process.exitCode = 1;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const masked2 = maskedDisplay(value2);
|
|
1444
|
+
process.stdout.write(
|
|
1445
|
+
`${JSON.stringify({ org, backend: backend2, stored: true, masked: masked2, updated_at: updatedAt2 })}
|
|
1446
|
+
`
|
|
1447
|
+
);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1381
1450
|
let backend;
|
|
1382
1451
|
let value;
|
|
1383
1452
|
try {
|
|
@@ -1393,39 +1462,25 @@ async function handleStatus(options, org) {
|
|
|
1393
1462
|
throw err;
|
|
1394
1463
|
}
|
|
1395
1464
|
const storedEvents = readAuditEvents().filter((ev) => ev.org === org && ev.event === "auth.store");
|
|
1396
|
-
const last = storedEvents
|
|
1465
|
+
const last = storedEvents.at(-1);
|
|
1397
1466
|
const updatedAt = last?.ts ?? null;
|
|
1398
1467
|
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}
|
|
1468
|
+
process.stdout.write(`Organization: ${org}
|
|
1406
1469
|
Backend: ${backend}
|
|
1407
1470
|
Stored: no
|
|
1408
1471
|
`);
|
|
1409
|
-
}
|
|
1410
1472
|
process.exitCode = 1;
|
|
1411
1473
|
return;
|
|
1412
1474
|
}
|
|
1413
1475
|
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}
|
|
1476
|
+
process.stdout.write(
|
|
1477
|
+
`Organization: ${org}
|
|
1422
1478
|
Backend: ${backend}
|
|
1423
1479
|
Stored: yes
|
|
1424
1480
|
Identifier: ${masked}
|
|
1425
1481
|
` + (updatedAt ? `Last updated: ${updatedAt}
|
|
1426
1482
|
` : "")
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1483
|
+
);
|
|
1429
1484
|
}
|
|
1430
1485
|
async function handleLogout(options, orgFromGlobal) {
|
|
1431
1486
|
if (options.all && orgFromGlobal) {
|
|
@@ -1434,9 +1489,16 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1434
1489
|
return;
|
|
1435
1490
|
}
|
|
1436
1491
|
if (options.all) {
|
|
1437
|
-
let orgs;
|
|
1438
1492
|
try {
|
|
1439
|
-
|
|
1493
|
+
const result = await logout({ all: true });
|
|
1494
|
+
if (result.removed.length === 0) {
|
|
1495
|
+
process.stdout.write("No stored credentials to remove.\n");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
for (const r of result.removed) {
|
|
1499
|
+
process.stdout.write(`Removed ${r.kind} credential for org ${r.org}.
|
|
1500
|
+
`);
|
|
1501
|
+
}
|
|
1440
1502
|
} catch (err) {
|
|
1441
1503
|
if (err instanceof CredentialStoreUnavailableError) {
|
|
1442
1504
|
process.stderr.write(`${err.message}
|
|
@@ -1444,22 +1506,9 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1444
1506
|
process.exitCode = 4;
|
|
1445
1507
|
return;
|
|
1446
1508
|
}
|
|
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}.
|
|
1509
|
+
process.stderr.write(`Failed to remove credentials: ${err.message}
|
|
1457
1510
|
`);
|
|
1458
|
-
|
|
1459
|
-
process.stderr.write(`Failed to remove PAT for org ${org}: ${err.message}
|
|
1460
|
-
`);
|
|
1461
|
-
process.exitCode = 1;
|
|
1462
|
-
}
|
|
1511
|
+
process.exitCode = 1;
|
|
1463
1512
|
}
|
|
1464
1513
|
return;
|
|
1465
1514
|
}
|
|
@@ -1473,10 +1522,10 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1473
1522
|
try {
|
|
1474
1523
|
const removed = await deletePat(resolved.org);
|
|
1475
1524
|
if (removed) {
|
|
1476
|
-
process.stdout.write(`
|
|
1525
|
+
process.stdout.write(`Credential removed for org ${resolved.org}.
|
|
1477
1526
|
`);
|
|
1478
1527
|
} else {
|
|
1479
|
-
process.stdout.write(`No stored
|
|
1528
|
+
process.stdout.write(`No stored credential for org ${resolved.org}.
|
|
1480
1529
|
`);
|
|
1481
1530
|
}
|
|
1482
1531
|
} catch (err) {
|
|
@@ -1491,14 +1540,70 @@ async function handleLogout(options, orgFromGlobal) {
|
|
|
1491
1540
|
}
|
|
1492
1541
|
function createAuthCommand() {
|
|
1493
1542
|
const command = new Command3("auth");
|
|
1494
|
-
command.description(
|
|
1495
|
-
|
|
1496
|
-
|
|
1543
|
+
command.description(
|
|
1544
|
+
"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."
|
|
1545
|
+
);
|
|
1546
|
+
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)");
|
|
1547
|
+
command.addHelpText(
|
|
1548
|
+
"after",
|
|
1549
|
+
`
|
|
1550
|
+
Default flow (OAuth, browser-based):
|
|
1551
|
+
azdo auth login --org <name>
|
|
1552
|
+
\u2192 opens the default browser for OAuth (Microsoft Entra v2 + PKCE).
|
|
1553
|
+
|
|
1554
|
+
Headless / no-browser:
|
|
1555
|
+
azdo auth login --org <name> --device-code
|
|
1556
|
+
|
|
1557
|
+
PAT path (legacy, opt-in):
|
|
1558
|
+
azdo auth login --org <name> --use-pat
|
|
1559
|
+
azdo auth --org <name> # back-compat alias of the above
|
|
1560
|
+
|
|
1561
|
+
OAuth scope set \u2014 shipped first-party client (default install):
|
|
1562
|
+
${firstPartyShippedScopes().join("\n ")}
|
|
1563
|
+
(uses ${AZDO_RESOURCE_ID}/.default \u2014 per-scope consent is unavailable
|
|
1564
|
+
against a client we do not own; .default grants the VS client's
|
|
1565
|
+
pre-authorized AzDO permissions in one step.)
|
|
1566
|
+
|
|
1567
|
+
OAuth scope set \u2014 self-registered apps (--client-id / AZDO_OAUTH_CLIENT_ID):
|
|
1568
|
+
${defaultScopes().join("\n ")}
|
|
1569
|
+
(FR-016, mirrors the PAT scope table \u2014 see docs/oauth-app-registration.md)
|
|
1570
|
+
|
|
1571
|
+
For self-registered OAuth apps (locked-down tenants), see docs/oauth-app-registration.md
|
|
1572
|
+
\u2014 that same guide is the maintainer reference for the project's shared client id.
|
|
1573
|
+
|
|
1574
|
+
Note: stored credentials may coexist as 'pat' or 'oauth' across orgs (FR-007).
|
|
1575
|
+
Note: \`azdo auth\` (no subcommand) preserves the legacy PAT-prompt entry point;
|
|
1576
|
+
\`azdo auth login\` is the spec-canonical name and defaults to OAuth.`
|
|
1577
|
+
).action(async (options) => {
|
|
1497
1578
|
await handleAuthRoot(options);
|
|
1498
1579
|
});
|
|
1499
|
-
const
|
|
1580
|
+
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)");
|
|
1581
|
+
loginCmd.action(async () => {
|
|
1582
|
+
const merged = loginCmd.optsWithGlobals();
|
|
1583
|
+
await handleLoginSubcommand(merged);
|
|
1584
|
+
});
|
|
1585
|
+
const statusCmd = command.command("status").description("Report stored credentials (kind, org, account/expiry, backend) \u2014 never the token").option("--json", "emit JSON", false);
|
|
1500
1586
|
statusCmd.action(async (options) => {
|
|
1501
1587
|
const globals = statusCmd.optsWithGlobals();
|
|
1588
|
+
if (!globals.org) {
|
|
1589
|
+
if (options.json) {
|
|
1590
|
+
await handleStatusJson();
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const report = await status();
|
|
1594
|
+
if (report.orgs.length === 0) {
|
|
1595
|
+
process.stdout.write("No stored credentials.\n");
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
for (const e of report.orgs) {
|
|
1599
|
+
const expiry = e.expiresAt ? new Date(e.expiresAt * 1e3).toISOString() : "n/a";
|
|
1600
|
+
process.stdout.write(
|
|
1601
|
+
`${e.org} ${e.kind} ${e.accountId ?? ""} ${expiry}
|
|
1602
|
+
`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1502
1607
|
const resolved = resolveOrg({ org: globals.org });
|
|
1503
1608
|
if (!resolved) {
|
|
1504
1609
|
process.stderr.write(`${formatResolutionError()}
|
|
@@ -1508,7 +1613,11 @@ function createAuthCommand() {
|
|
|
1508
1613
|
}
|
|
1509
1614
|
await handleStatus(options, resolved.org);
|
|
1510
1615
|
});
|
|
1511
|
-
|
|
1616
|
+
statusCmd.addHelpText(
|
|
1617
|
+
"after",
|
|
1618
|
+
"\nStored credentials may be of kind `pat` or `oauth` and may coexist across orgs (FR-007).\n"
|
|
1619
|
+
);
|
|
1620
|
+
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
1621
|
logoutCmd.action(async (options) => {
|
|
1513
1622
|
const globals = logoutCmd.optsWithGlobals();
|
|
1514
1623
|
await handleLogout(options, globals.org);
|
|
@@ -1690,11 +1799,11 @@ function createSetStateCommand() {
|
|
|
1690
1799
|
let context;
|
|
1691
1800
|
try {
|
|
1692
1801
|
context = resolveContext(options);
|
|
1693
|
-
const credential = await
|
|
1802
|
+
const credential = await requireAuthCredential(context.org);
|
|
1694
1803
|
const operations = [
|
|
1695
1804
|
{ op: "add", path: "/fields/System.State", value: state }
|
|
1696
1805
|
];
|
|
1697
|
-
const result = await updateWorkItem(context, id, credential
|
|
1806
|
+
const result = await updateWorkItem(context, id, credential, "System.State", operations);
|
|
1698
1807
|
if (options.json) {
|
|
1699
1808
|
process.stdout.write(
|
|
1700
1809
|
JSON.stringify({
|
|
@@ -1740,12 +1849,12 @@ function createAssignCommand() {
|
|
|
1740
1849
|
let context;
|
|
1741
1850
|
try {
|
|
1742
1851
|
context = resolveContext(options);
|
|
1743
|
-
const credential = await
|
|
1852
|
+
const credential = await requireAuthCredential(context.org);
|
|
1744
1853
|
const value = options.unassign ? "" : name;
|
|
1745
1854
|
const operations = [
|
|
1746
1855
|
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
1747
1856
|
];
|
|
1748
|
-
const result = await updateWorkItem(context, id, credential
|
|
1857
|
+
const result = await updateWorkItem(context, id, credential, "System.AssignedTo", operations);
|
|
1749
1858
|
if (options.json) {
|
|
1750
1859
|
process.stdout.write(
|
|
1751
1860
|
JSON.stringify({
|
|
@@ -1780,11 +1889,11 @@ function createSetFieldCommand() {
|
|
|
1780
1889
|
let context;
|
|
1781
1890
|
try {
|
|
1782
1891
|
context = resolveContext(options);
|
|
1783
|
-
const credential = await
|
|
1892
|
+
const credential = await requireAuthCredential(context.org);
|
|
1784
1893
|
const operations = [
|
|
1785
1894
|
{ op: "add", path: `/fields/${field}`, value }
|
|
1786
1895
|
];
|
|
1787
|
-
const result = await updateWorkItem(context, id, credential
|
|
1896
|
+
const result = await updateWorkItem(context, id, credential, field, operations);
|
|
1788
1897
|
if (options.json) {
|
|
1789
1898
|
process.stdout.write(
|
|
1790
1899
|
JSON.stringify({
|
|
@@ -1818,8 +1927,8 @@ function createGetMdFieldCommand() {
|
|
|
1818
1927
|
let context;
|
|
1819
1928
|
try {
|
|
1820
1929
|
context = resolveContext(options);
|
|
1821
|
-
const credential = await
|
|
1822
|
-
const value = await getWorkItemFieldValue(context, id, credential
|
|
1930
|
+
const credential = await requireAuthCredential(context.org);
|
|
1931
|
+
const value = await getWorkItemFieldValue(context, id, credential, field);
|
|
1823
1932
|
if (value === null) {
|
|
1824
1933
|
process.stdout.write("\n");
|
|
1825
1934
|
} else {
|
|
@@ -1906,12 +2015,12 @@ function createSetMdFieldCommand() {
|
|
|
1906
2015
|
let context;
|
|
1907
2016
|
try {
|
|
1908
2017
|
context = resolveContext(options);
|
|
1909
|
-
const credential = await
|
|
2018
|
+
const credential = await requireAuthCredential(context.org);
|
|
1910
2019
|
const operations = [
|
|
1911
2020
|
{ op: "add", path: `/fields/${field}`, value: content },
|
|
1912
2021
|
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
1913
2022
|
];
|
|
1914
|
-
const result = await updateWorkItem(context, id, credential
|
|
2023
|
+
const result = await updateWorkItem(context, id, credential, field, operations);
|
|
1915
2024
|
formatOutput(result, options, field);
|
|
1916
2025
|
} catch (err) {
|
|
1917
2026
|
handleCommandError(err, id, context, "write");
|
|
@@ -2222,15 +2331,15 @@ function createUpsertCommand() {
|
|
|
2222
2331
|
ensureTitleForCreate(document.fields);
|
|
2223
2332
|
}
|
|
2224
2333
|
const operations = toPatchOperations(document.fields, action);
|
|
2225
|
-
const credential = await
|
|
2334
|
+
const credential = await requireAuthCredential(context.org);
|
|
2226
2335
|
let writeResult;
|
|
2227
2336
|
if (action === "created") {
|
|
2228
|
-
writeResult = await createWorkItem(context, createType, credential
|
|
2337
|
+
writeResult = await createWorkItem(context, createType, credential, operations);
|
|
2229
2338
|
} else {
|
|
2230
2339
|
if (id === void 0) {
|
|
2231
2340
|
fail2("Work item ID is required for updates.");
|
|
2232
2341
|
}
|
|
2233
|
-
writeResult = await applyWorkItemPatch(context, id, credential
|
|
2342
|
+
writeResult = await applyWorkItemPatch(context, id, credential, operations);
|
|
2234
2343
|
}
|
|
2235
2344
|
const result = buildUpsertResult(
|
|
2236
2345
|
action,
|
|
@@ -2288,8 +2397,8 @@ function createListFieldsCommand() {
|
|
|
2288
2397
|
let context;
|
|
2289
2398
|
try {
|
|
2290
2399
|
context = resolveContext(options);
|
|
2291
|
-
const credential = await
|
|
2292
|
-
const fields = await getWorkItemFields(context, id, credential
|
|
2400
|
+
const credential = await requireAuthCredential(context.org);
|
|
2401
|
+
const fields = await getWorkItemFields(context, id, credential);
|
|
2293
2402
|
if (options.json) {
|
|
2294
2403
|
process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
|
|
2295
2404
|
} else {
|
|
@@ -2343,9 +2452,9 @@ function mapPullRequest(repo, pullRequest) {
|
|
|
2343
2452
|
url: pullRequest._links?.web?.href ?? null
|
|
2344
2453
|
};
|
|
2345
2454
|
}
|
|
2346
|
-
function mapPullRequestCheckName(
|
|
2347
|
-
const genre =
|
|
2348
|
-
const name =
|
|
2455
|
+
function mapPullRequestCheckName(status2) {
|
|
2456
|
+
const genre = status2.context?.genre?.trim();
|
|
2457
|
+
const name = status2.context?.name?.trim();
|
|
2349
2458
|
if (genre && name) {
|
|
2350
2459
|
return `${genre}/${name}`;
|
|
2351
2460
|
}
|
|
@@ -2355,21 +2464,21 @@ function mapPullRequestCheckName(status) {
|
|
|
2355
2464
|
if (genre) {
|
|
2356
2465
|
return genre;
|
|
2357
2466
|
}
|
|
2358
|
-
return `Status #${
|
|
2467
|
+
return `Status #${status2.id}`;
|
|
2359
2468
|
}
|
|
2360
|
-
function mapPullRequestCheck(
|
|
2361
|
-
if (
|
|
2469
|
+
function mapPullRequestCheck(status2) {
|
|
2470
|
+
if (status2.state === "notApplicable" || status2.state === "notSet") {
|
|
2362
2471
|
return null;
|
|
2363
2472
|
}
|
|
2364
2473
|
return {
|
|
2365
|
-
id:
|
|
2366
|
-
state:
|
|
2367
|
-
name: mapPullRequestCheckName(
|
|
2368
|
-
description:
|
|
2369
|
-
targetUrl:
|
|
2370
|
-
createdBy:
|
|
2371
|
-
createdAt:
|
|
2372
|
-
updatedAt:
|
|
2474
|
+
id: status2.id,
|
|
2475
|
+
state: status2.state,
|
|
2476
|
+
name: mapPullRequestCheckName(status2),
|
|
2477
|
+
description: status2.description ?? null,
|
|
2478
|
+
targetUrl: status2.targetUrl ?? null,
|
|
2479
|
+
createdBy: status2.createdBy?.displayName ?? null,
|
|
2480
|
+
createdAt: status2.creationDate ?? null,
|
|
2481
|
+
updatedAt: status2.updatedDate ?? null
|
|
2373
2482
|
};
|
|
2374
2483
|
}
|
|
2375
2484
|
function mapComment(comment) {
|
|
@@ -2405,8 +2514,8 @@ function toActiveCommentThread(thread) {
|
|
|
2405
2514
|
};
|
|
2406
2515
|
}
|
|
2407
2516
|
var RESOLVED_THREAD_STATUSES = /* @__PURE__ */ new Set(["fixed", "wontFix", "closed", "byDesign"]);
|
|
2408
|
-
function isThreadResolved(
|
|
2409
|
-
return RESOLVED_THREAD_STATUSES.has(
|
|
2517
|
+
function isThreadResolved(status2) {
|
|
2518
|
+
return RESOLVED_THREAD_STATUSES.has(status2);
|
|
2410
2519
|
}
|
|
2411
2520
|
async function readJsonResponse(response) {
|
|
2412
2521
|
if (!response.ok) {
|
|
@@ -2414,7 +2523,7 @@ async function readJsonResponse(response) {
|
|
|
2414
2523
|
}
|
|
2415
2524
|
return response.json();
|
|
2416
2525
|
}
|
|
2417
|
-
async function patchThreadStatus(context, repo,
|
|
2526
|
+
async function patchThreadStatus(context, repo, cred, prId, threadId, status2) {
|
|
2418
2527
|
const url = new URL(
|
|
2419
2528
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}`
|
|
2420
2529
|
);
|
|
@@ -2422,41 +2531,41 @@ async function patchThreadStatus(context, repo, pat, prId, threadId, status) {
|
|
|
2422
2531
|
const response = await fetchWithErrors(url.toString(), {
|
|
2423
2532
|
method: "PATCH",
|
|
2424
2533
|
headers: {
|
|
2425
|
-
...authHeaders(
|
|
2534
|
+
...authHeaders(cred),
|
|
2426
2535
|
"Content-Type": "application/json"
|
|
2427
2536
|
},
|
|
2428
|
-
body: JSON.stringify({ status })
|
|
2537
|
+
body: JSON.stringify({ status: status2 })
|
|
2429
2538
|
});
|
|
2430
2539
|
const data = await readJsonResponse(response);
|
|
2431
2540
|
return toActiveCommentThread(data);
|
|
2432
2541
|
}
|
|
2433
|
-
async function getPullRequestById(context, repo,
|
|
2542
|
+
async function getPullRequestById(context, repo, cred, prId) {
|
|
2434
2543
|
const url = new URL(
|
|
2435
2544
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}`
|
|
2436
2545
|
);
|
|
2437
2546
|
url.searchParams.set("api-version", "7.1");
|
|
2438
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
2547
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
2439
2548
|
const data = await readJsonResponse(response);
|
|
2440
2549
|
return mapPullRequest(repo, data);
|
|
2441
2550
|
}
|
|
2442
|
-
async function listPullRequests(context, repo,
|
|
2551
|
+
async function listPullRequests(context, repo, cred, sourceBranch, opts) {
|
|
2443
2552
|
const response = await fetchWithErrors(
|
|
2444
2553
|
buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
|
|
2445
|
-
{ headers: authHeaders(
|
|
2554
|
+
{ headers: authHeaders(cred) }
|
|
2446
2555
|
);
|
|
2447
2556
|
const data = await readJsonResponse(response);
|
|
2448
2557
|
return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
|
|
2449
2558
|
}
|
|
2450
|
-
async function getPullRequestChecks(context, repo,
|
|
2559
|
+
async function getPullRequestChecks(context, repo, cred, prId) {
|
|
2451
2560
|
const response = await fetchWithErrors(
|
|
2452
2561
|
buildPullRequestStatusesUrl(context, repo, prId).toString(),
|
|
2453
|
-
{ headers: authHeaders(
|
|
2562
|
+
{ headers: authHeaders(cred) }
|
|
2454
2563
|
);
|
|
2455
2564
|
const data = await readJsonResponse(response);
|
|
2456
2565
|
return data.value.map(mapPullRequestCheck).filter((check) => check !== null);
|
|
2457
2566
|
}
|
|
2458
|
-
async function openPullRequest(context, repo,
|
|
2459
|
-
const existing = await listPullRequests(context, repo,
|
|
2567
|
+
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
2568
|
+
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
2460
2569
|
status: "active",
|
|
2461
2570
|
targetBranch: "develop"
|
|
2462
2571
|
});
|
|
@@ -2484,7 +2593,7 @@ async function openPullRequest(context, repo, pat, sourceBranch, title, descript
|
|
|
2484
2593
|
const response = await fetchWithErrors(url.toString(), {
|
|
2485
2594
|
method: "POST",
|
|
2486
2595
|
headers: {
|
|
2487
|
-
...authHeaders(
|
|
2596
|
+
...authHeaders(cred),
|
|
2488
2597
|
"Content-Type": "application/json"
|
|
2489
2598
|
},
|
|
2490
2599
|
body: JSON.stringify(payload)
|
|
@@ -2497,12 +2606,12 @@ async function openPullRequest(context, repo, pat, sourceBranch, title, descript
|
|
|
2497
2606
|
pullRequest: mapPullRequest(repo, data)
|
|
2498
2607
|
};
|
|
2499
2608
|
}
|
|
2500
|
-
async function getPullRequestThreads(context, repo,
|
|
2609
|
+
async function getPullRequestThreads(context, repo, cred, prId) {
|
|
2501
2610
|
const url = new URL(
|
|
2502
2611
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
2503
2612
|
);
|
|
2504
2613
|
url.searchParams.set("api-version", "7.1");
|
|
2505
|
-
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(
|
|
2614
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
2506
2615
|
const data = await readJsonResponse(response);
|
|
2507
2616
|
return data.value.map(mapThread).filter((thread) => thread !== null);
|
|
2508
2617
|
}
|
|
@@ -2569,8 +2678,8 @@ function formatPullRequestBlock(pullRequest) {
|
|
|
2569
2678
|
...formatPullRequestChecks(pullRequest.checks)
|
|
2570
2679
|
].join("\n");
|
|
2571
2680
|
}
|
|
2572
|
-
function threadStatusLabel(
|
|
2573
|
-
return isThreadResolved(
|
|
2681
|
+
function threadStatusLabel(status2) {
|
|
2682
|
+
return isThreadResolved(status2) ? "resolved" : status2;
|
|
2574
2683
|
}
|
|
2575
2684
|
function formatThreads(prId, title, threads) {
|
|
2576
2685
|
const lines = [`Comment threads for pull request #${prId}: ${title}`];
|
|
@@ -2587,12 +2696,12 @@ async function resolvePrCommandContext(options, resolveOpts = {}) {
|
|
|
2587
2696
|
const context = resolveContext(options);
|
|
2588
2697
|
const repo = detectRepoName();
|
|
2589
2698
|
const branch = requireBranch ? getCurrentBranch() : null;
|
|
2590
|
-
const credential = await
|
|
2699
|
+
const credential = await requireAuthCredential(context.org);
|
|
2591
2700
|
return {
|
|
2592
2701
|
context,
|
|
2593
2702
|
repo,
|
|
2594
2703
|
branch,
|
|
2595
|
-
pat: credential
|
|
2704
|
+
pat: credential
|
|
2596
2705
|
};
|
|
2597
2706
|
}
|
|
2598
2707
|
function createPrStatusCommand() {
|
|
@@ -2915,8 +3024,8 @@ function createCommentsListCommand() {
|
|
|
2915
3024
|
let context;
|
|
2916
3025
|
try {
|
|
2917
3026
|
context = resolveContext(options);
|
|
2918
|
-
const credential = await
|
|
2919
|
-
const result = await listWorkItemComments(context, id, credential
|
|
3027
|
+
const credential = await requireAuthCredential(context.org);
|
|
3028
|
+
const result = await listWorkItemComments(context, id, credential);
|
|
2920
3029
|
if (options.json) {
|
|
2921
3030
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2922
3031
|
`);
|
|
@@ -2946,9 +3055,9 @@ function createCommentsAddCommand() {
|
|
|
2946
3055
|
let context;
|
|
2947
3056
|
try {
|
|
2948
3057
|
context = resolveContext(options);
|
|
2949
|
-
const credential = await
|
|
3058
|
+
const credential = await requireAuthCredential(context.org);
|
|
2950
3059
|
const format = options.markdown === true ? "markdown" : "html";
|
|
2951
|
-
const result = await addWorkItemComment(context, id, credential
|
|
3060
|
+
const result = await addWorkItemComment(context, id, credential, text, format);
|
|
2952
3061
|
if (options.json) {
|
|
2953
3062
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2954
3063
|
`);
|
|
@@ -2984,14 +3093,14 @@ function createDownloadAttachmentCommand() {
|
|
|
2984
3093
|
let context;
|
|
2985
3094
|
try {
|
|
2986
3095
|
context = resolveContext(options);
|
|
2987
|
-
const credential = await
|
|
3096
|
+
const credential = await requireAuthCredential(context.org);
|
|
2988
3097
|
const outputDir = options.output ?? ".";
|
|
2989
3098
|
if (!existsSync4(outputDir)) {
|
|
2990
3099
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
2991
3100
|
`);
|
|
2992
3101
|
process.exit(1);
|
|
2993
3102
|
}
|
|
2994
|
-
const workItem = await getWorkItem(context, id, credential
|
|
3103
|
+
const workItem = await getWorkItem(context, id, credential);
|
|
2995
3104
|
const attachment = workItem.attachments?.find(
|
|
2996
3105
|
(a) => a.name === filename
|
|
2997
3106
|
);
|
|
@@ -3002,7 +3111,7 @@ function createDownloadAttachmentCommand() {
|
|
|
3002
3111
|
);
|
|
3003
3112
|
process.exit(1);
|
|
3004
3113
|
}
|
|
3005
|
-
const data = await downloadAttachment(attachment.url, credential
|
|
3114
|
+
const data = await downloadAttachment(attachment.url, credential);
|
|
3006
3115
|
const outputPath = join2(outputDir, filename);
|
|
3007
3116
|
await writeFile(outputPath, Buffer.from(data));
|
|
3008
3117
|
process.stdout.write(
|