azdo-cli 0.10.0-develop.213 → 0.10.0-develop.233
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 +1 -1
- package/dist/index.js +706 -183
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
16
16
|
- Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
|
|
17
17
|
- Persist org/project/default fields in local config (`config`)
|
|
18
18
|
- List all fields of a work item (`list-fields`)
|
|
19
|
-
- Store PAT in OS credential store (or use `AZDO_PAT`)
|
|
19
|
+
- Store a PAT per Azure DevOps organization in the OS credential store via `azdo auth` (or use `AZDO_PAT`). Inspect with `azdo auth status`, remove with `azdo auth logout`. See [docs/authentication.md](docs/authentication.md).
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command15 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -367,42 +367,182 @@ import { dirname as dirname2, join } from "path";
|
|
|
367
367
|
|
|
368
368
|
// src/services/credential-store.ts
|
|
369
369
|
import { Entry } from "@napi-rs/keyring";
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
|
|
371
|
+
// src/types/credential.ts
|
|
372
|
+
var CredentialStoreUnavailableError = class extends Error {
|
|
373
|
+
backend;
|
|
374
|
+
constructor(backend, cause) {
|
|
375
|
+
super(`OS secret backend unavailable (${backend}). Install the platform's credential service and try again.`);
|
|
376
|
+
this.name = "CredentialStoreUnavailableError";
|
|
377
|
+
this.backend = backend;
|
|
378
|
+
if (cause instanceof Error) {
|
|
379
|
+
this.cause = cause;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/services/audit-log.ts
|
|
385
|
+
import fs from "fs";
|
|
386
|
+
import os from "os";
|
|
387
|
+
import path from "path";
|
|
388
|
+
function getAuditLogPath() {
|
|
389
|
+
return path.join(os.homedir(), ".azdo", "audit.log");
|
|
390
|
+
}
|
|
391
|
+
function ensureDirWithPerms(dir) {
|
|
392
|
+
if (!fs.existsSync(dir)) {
|
|
393
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
373
396
|
try {
|
|
374
|
-
|
|
375
|
-
return entry.getPassword();
|
|
397
|
+
fs.chmodSync(dir, 448);
|
|
376
398
|
} catch {
|
|
377
|
-
return null;
|
|
378
399
|
}
|
|
379
400
|
}
|
|
380
|
-
|
|
401
|
+
function ensureFileWithPerms(file) {
|
|
402
|
+
if (!fs.existsSync(file)) {
|
|
403
|
+
fs.writeFileSync(file, "", { mode: 384 });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
381
406
|
try {
|
|
382
|
-
|
|
383
|
-
entry.setPassword(pat);
|
|
384
|
-
return true;
|
|
407
|
+
fs.chmodSync(file, 384);
|
|
385
408
|
} catch {
|
|
386
|
-
return false;
|
|
387
409
|
}
|
|
388
410
|
}
|
|
389
|
-
|
|
411
|
+
function appendAuthAuditEvent(input) {
|
|
412
|
+
const auditLog = getAuditLogPath();
|
|
413
|
+
const dir = path.dirname(auditLog);
|
|
414
|
+
ensureDirWithPerms(dir);
|
|
415
|
+
ensureFileWithPerms(auditLog);
|
|
416
|
+
const record = {
|
|
417
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
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
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
482
|
+
function getConfigPath() {
|
|
483
|
+
return path2.join(os2.homedir(), ".azdo", "config.json");
|
|
484
|
+
}
|
|
485
|
+
function loadConfig() {
|
|
486
|
+
const configPath = getConfigPath();
|
|
487
|
+
let raw;
|
|
488
|
+
try {
|
|
489
|
+
raw = fs2.readFileSync(configPath, "utf-8");
|
|
490
|
+
} catch (err) {
|
|
491
|
+
if (err.code === "ENOENT") {
|
|
492
|
+
return {};
|
|
493
|
+
}
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
390
496
|
try {
|
|
391
|
-
|
|
392
|
-
entry.deletePassword();
|
|
393
|
-
return true;
|
|
497
|
+
return JSON.parse(raw);
|
|
394
498
|
} catch {
|
|
395
|
-
|
|
499
|
+
process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
|
|
500
|
+
`);
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function saveConfig(config) {
|
|
505
|
+
const configPath = getConfigPath();
|
|
506
|
+
const dir = path2.dirname(configPath);
|
|
507
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
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(", ")}`);
|
|
396
513
|
}
|
|
397
514
|
}
|
|
515
|
+
function getConfigValue(key) {
|
|
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
|
+
}
|
|
398
543
|
|
|
399
|
-
// src/services/auth.ts
|
|
400
|
-
var PAT_PROMPT = "Enter your Azure DevOps PAT: ";
|
|
544
|
+
// src/services/auth-masking.ts
|
|
401
545
|
var VISIBLE_CHARS = 5;
|
|
402
|
-
function normalizePat(rawPat) {
|
|
403
|
-
const trimmedPat = rawPat.trim();
|
|
404
|
-
return trimmedPat.length > 0 ? trimmedPat : null;
|
|
405
|
-
}
|
|
406
546
|
function maskedDisplay(pat) {
|
|
407
547
|
if (pat.length <= VISIBLE_CHARS * 2) {
|
|
408
548
|
return pat;
|
|
@@ -410,6 +550,142 @@ function maskedDisplay(pat) {
|
|
|
410
550
|
const hiddenCount = pat.length - VISIBLE_CHARS * 2;
|
|
411
551
|
return pat.slice(0, VISIBLE_CHARS) + "*".repeat(hiddenCount) + pat.slice(-VISIBLE_CHARS);
|
|
412
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);
|
|
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'
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
async function maybeMigrateLegacy(targetOrg) {
|
|
592
|
+
const config = loadConfig();
|
|
593
|
+
if (!config.org || config.org !== targetOrg) {
|
|
594
|
+
if (!config.org) {
|
|
595
|
+
let legacyExists;
|
|
596
|
+
try {
|
|
597
|
+
const legacyEntry2 = new Entry(SERVICE, LEGACY_ACCOUNT);
|
|
598
|
+
legacyExists = legacyEntry2.getPassword() !== null;
|
|
599
|
+
} catch {
|
|
600
|
+
legacyExists = false;
|
|
601
|
+
}
|
|
602
|
+
if (legacyExists) {
|
|
603
|
+
emitLegacyUnsetNoticeOnce();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
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
|
+
}
|
|
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)
|
|
663
|
+
});
|
|
664
|
+
return true;
|
|
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;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/services/auth.ts
|
|
688
|
+
var PAT_PROMPT = "Enter your Azure DevOps PAT: ";
|
|
413
689
|
async function promptForPat() {
|
|
414
690
|
if (!process.stdin.isTTY) {
|
|
415
691
|
return null;
|
|
@@ -419,7 +695,6 @@ async function promptForPat() {
|
|
|
419
695
|
input: process.stdin,
|
|
420
696
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
421
697
|
output: null
|
|
422
|
-
// null disables readline's automatic echo
|
|
423
698
|
});
|
|
424
699
|
process.stderr.write(PAT_PROMPT);
|
|
425
700
|
process.stdin.setRawMode(true);
|
|
@@ -475,12 +750,12 @@ function findDotEnvPat(startDir = process.cwd()) {
|
|
|
475
750
|
}
|
|
476
751
|
return null;
|
|
477
752
|
}
|
|
478
|
-
async function resolvePat(
|
|
753
|
+
async function resolvePat(org) {
|
|
479
754
|
const envPat = process.env.AZDO_PAT;
|
|
480
|
-
if (envPat) {
|
|
755
|
+
if (envPat && envPat.length > 0) {
|
|
481
756
|
return { pat: envPat, source: "env" };
|
|
482
757
|
}
|
|
483
|
-
const storedPat = await getPat();
|
|
758
|
+
const storedPat = await getPat(org);
|
|
484
759
|
if (storedPat !== null) {
|
|
485
760
|
return { pat: storedPat, source: "credential-store" };
|
|
486
761
|
}
|
|
@@ -488,21 +763,34 @@ async function resolvePat(promptFn = promptForPat) {
|
|
|
488
763
|
if (dotEnvPat !== null) {
|
|
489
764
|
return { pat: dotEnvPat, source: "env" };
|
|
490
765
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
process.stderr.write("Warning: Could not save PAT to credential store. You may need to enter it again next time.\n");
|
|
498
|
-
}
|
|
499
|
-
return { pat: normalizedPat, source: "prompt" };
|
|
500
|
-
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
async function requirePat(org) {
|
|
769
|
+
const cred = await resolvePat(org);
|
|
770
|
+
if (cred !== null) {
|
|
771
|
+
return cred;
|
|
501
772
|
}
|
|
502
773
|
throw new Error(
|
|
503
|
-
"
|
|
774
|
+
`No PAT available for org "${org}". Set AZDO_PAT environment variable or run \`azdo auth --org ${org}\`.`
|
|
504
775
|
);
|
|
505
776
|
}
|
|
777
|
+
async function validatePatAgainstAzdo(pat, org) {
|
|
778
|
+
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?$top=1&api-version=7.1`;
|
|
779
|
+
const auth = Buffer.from(`:${pat}`).toString("base64");
|
|
780
|
+
const response = await fetch(url, {
|
|
781
|
+
headers: {
|
|
782
|
+
Authorization: `Basic ${auth}`,
|
|
783
|
+
Accept: "application/json"
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
if (response.status === 200) {
|
|
787
|
+
return { ok: true, status: 200 };
|
|
788
|
+
}
|
|
789
|
+
if (response.status === 401 || response.status === 403) {
|
|
790
|
+
return { ok: false, status: response.status };
|
|
791
|
+
}
|
|
792
|
+
throw new Error(`Azure DevOps returned HTTP ${response.status} while validating PAT for org "${org}".`);
|
|
793
|
+
}
|
|
506
794
|
|
|
507
795
|
// src/services/git-remote.ts
|
|
508
796
|
import { execSync } from "child_process";
|
|
@@ -574,122 +862,57 @@ function getCurrentBranch() {
|
|
|
574
862
|
return branch;
|
|
575
863
|
}
|
|
576
864
|
|
|
577
|
-
// src/services/
|
|
578
|
-
|
|
579
|
-
import path from "path";
|
|
580
|
-
import os from "os";
|
|
581
|
-
var SETTINGS = [
|
|
582
|
-
{
|
|
583
|
-
key: "org",
|
|
584
|
-
description: "Azure DevOps organization name",
|
|
585
|
-
type: "string",
|
|
586
|
-
example: "mycompany",
|
|
587
|
-
required: true
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
key: "project",
|
|
591
|
-
description: "Azure DevOps project name",
|
|
592
|
-
type: "string",
|
|
593
|
-
example: "MyProject",
|
|
594
|
-
required: true
|
|
595
|
-
},
|
|
596
|
-
{
|
|
597
|
-
key: "fields",
|
|
598
|
-
description: "Extra work item fields to include (comma-separated reference names)",
|
|
599
|
-
type: "string[]",
|
|
600
|
-
example: "System.Tags,Custom.Priority",
|
|
601
|
-
required: false
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
key: "markdown",
|
|
605
|
-
description: "Convert rich text fields to markdown on display",
|
|
606
|
-
type: "boolean",
|
|
607
|
-
example: "true",
|
|
608
|
-
required: false
|
|
609
|
-
}
|
|
610
|
-
];
|
|
611
|
-
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
612
|
-
function getConfigPath() {
|
|
613
|
-
return path.join(os.homedir(), ".azdo", "config.json");
|
|
614
|
-
}
|
|
615
|
-
function loadConfig() {
|
|
616
|
-
const configPath = getConfigPath();
|
|
617
|
-
let raw;
|
|
865
|
+
// src/services/org-resolver.ts
|
|
866
|
+
function defaultDetectFromGit() {
|
|
618
867
|
try {
|
|
619
|
-
|
|
620
|
-
} catch (err) {
|
|
621
|
-
if (err.code === "ENOENT") {
|
|
622
|
-
return {};
|
|
623
|
-
}
|
|
624
|
-
throw err;
|
|
625
|
-
}
|
|
626
|
-
try {
|
|
627
|
-
return JSON.parse(raw);
|
|
868
|
+
return detectAzdoContext().org ?? null;
|
|
628
869
|
} catch {
|
|
629
|
-
|
|
630
|
-
`);
|
|
631
|
-
return {};
|
|
870
|
+
return null;
|
|
632
871
|
}
|
|
633
872
|
}
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
const dir = path.dirname(configPath);
|
|
637
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
638
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
873
|
+
function defaultReadConfig() {
|
|
874
|
+
return loadConfig();
|
|
639
875
|
}
|
|
640
|
-
function
|
|
641
|
-
if (
|
|
642
|
-
|
|
876
|
+
function resolveOrg(options) {
|
|
877
|
+
if (options.org && options.org.length > 0) {
|
|
878
|
+
return { org: options.org, source: "flag" };
|
|
643
879
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const config = loadConfig();
|
|
648
|
-
return config[key];
|
|
649
|
-
}
|
|
650
|
-
function setConfigValue(key, value) {
|
|
651
|
-
validateKey(key);
|
|
652
|
-
const config = loadConfig();
|
|
653
|
-
if (value === "") {
|
|
654
|
-
delete config[key];
|
|
655
|
-
} else if (key === "markdown") {
|
|
656
|
-
if (value !== "true" && value !== "false") {
|
|
657
|
-
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
658
|
-
}
|
|
659
|
-
config.markdown = value === "true";
|
|
660
|
-
} else if (key === "fields") {
|
|
661
|
-
config.fields = value.split(",").map((s) => s.trim());
|
|
662
|
-
} else {
|
|
663
|
-
config[key] = value;
|
|
880
|
+
const gitOrg = (options.detectFromGit ?? defaultDetectFromGit)();
|
|
881
|
+
if (gitOrg && gitOrg.length > 0) {
|
|
882
|
+
return { org: gitOrg, source: "git" };
|
|
664
883
|
}
|
|
665
|
-
|
|
884
|
+
const configOrg = (options.readConfig ?? defaultReadConfig)().org;
|
|
885
|
+
if (configOrg && configOrg.length > 0) {
|
|
886
|
+
return { org: configOrg, source: "config" };
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
666
889
|
}
|
|
667
|
-
function
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
890
|
+
function formatResolutionError() {
|
|
891
|
+
return [
|
|
892
|
+
"Could not resolve an Azure DevOps organization. Options (in priority order):",
|
|
893
|
+
" 1. Pass --org <name> on the command line.",
|
|
894
|
+
" 2. Run this command from a git repo whose origin remote is an Azure DevOps URL.",
|
|
895
|
+
" 3. Run `azdo config set org <name>` once to set a persistent default."
|
|
896
|
+
].join("\n");
|
|
672
897
|
}
|
|
673
898
|
|
|
674
899
|
// src/services/context.ts
|
|
675
900
|
function resolveContext(options) {
|
|
676
|
-
|
|
677
|
-
return { org: options.org, project: options.project };
|
|
678
|
-
}
|
|
679
|
-
const config = loadConfig();
|
|
680
|
-
if (config.org && config.project) {
|
|
681
|
-
return { org: config.org, project: config.project };
|
|
682
|
-
}
|
|
901
|
+
const resolvedOrg = resolveOrg({ org: options.org });
|
|
683
902
|
let gitContext = null;
|
|
684
903
|
try {
|
|
685
904
|
gitContext = detectAzdoContext();
|
|
686
905
|
} catch {
|
|
687
906
|
}
|
|
688
|
-
const
|
|
689
|
-
const
|
|
907
|
+
const config = loadConfig();
|
|
908
|
+
const org = resolvedOrg?.org;
|
|
909
|
+
const project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || config.project;
|
|
690
910
|
if (org && project) {
|
|
691
911
|
return { org, project };
|
|
692
912
|
}
|
|
913
|
+
if (!org) {
|
|
914
|
+
throw new Error(formatResolutionError());
|
|
915
|
+
}
|
|
693
916
|
throw new Error(
|
|
694
917
|
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
695
918
|
);
|
|
@@ -965,7 +1188,7 @@ function createGetItemCommand() {
|
|
|
965
1188
|
let context;
|
|
966
1189
|
try {
|
|
967
1190
|
context = resolveContext(options);
|
|
968
|
-
const credential = await
|
|
1191
|
+
const credential = await requirePat(context.org);
|
|
969
1192
|
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
970
1193
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
971
1194
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
@@ -983,19 +1206,318 @@ function createGetItemCommand() {
|
|
|
983
1206
|
import { Command as Command2 } from "commander";
|
|
984
1207
|
function createClearPatCommand() {
|
|
985
1208
|
const command = new Command2("clear-pat");
|
|
986
|
-
command.description("Remove
|
|
987
|
-
|
|
1209
|
+
command.description("Remove a stored Azure DevOps PAT (deprecated: use `azdo auth logout`)").option("--org <name>", "Azure DevOps organization (overrides auto-detect / config)").action(async (options) => {
|
|
1210
|
+
process.stderr.write("`azdo clear-pat` is deprecated; use `azdo auth logout [--org <name>]` instead.\n");
|
|
1211
|
+
const resolved = resolveOrg({ org: options.org });
|
|
1212
|
+
if (!resolved) {
|
|
1213
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1214
|
+
`);
|
|
1215
|
+
process.exitCode = 3;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const deleted = await deletePat(resolved.org);
|
|
988
1219
|
if (deleted) {
|
|
989
|
-
process.stdout.write(
|
|
1220
|
+
process.stdout.write(`PAT removed for org ${resolved.org}.
|
|
1221
|
+
`);
|
|
990
1222
|
} else {
|
|
991
|
-
process.stdout.write(
|
|
1223
|
+
process.stdout.write(`No stored PAT found for org ${resolved.org}.
|
|
1224
|
+
`);
|
|
992
1225
|
}
|
|
993
1226
|
});
|
|
994
1227
|
return command;
|
|
995
1228
|
}
|
|
996
1229
|
|
|
997
|
-
// src/commands/
|
|
1230
|
+
// src/commands/auth.ts
|
|
998
1231
|
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
|
+
async function readStdinToString() {
|
|
1290
|
+
const chunks = [];
|
|
1291
|
+
for await (const chunk of process.stdin) {
|
|
1292
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1293
|
+
}
|
|
1294
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1295
|
+
}
|
|
1296
|
+
async function confirmOverwrite(org) {
|
|
1297
|
+
if (!process.stdin.isTTY) return true;
|
|
1298
|
+
process.stderr.write(`A PAT is already stored for org ${org}. Overwrite? [y/N] `);
|
|
1299
|
+
return await new Promise((resolve2) => {
|
|
1300
|
+
process.stdin.setEncoding("utf8");
|
|
1301
|
+
let answered = false;
|
|
1302
|
+
const handler = (data) => {
|
|
1303
|
+
if (answered) return;
|
|
1304
|
+
answered = true;
|
|
1305
|
+
process.stdin.removeListener("data", handler);
|
|
1306
|
+
process.stdin.pause();
|
|
1307
|
+
const trimmed = data.trim().toLowerCase();
|
|
1308
|
+
resolve2(trimmed === "y" || trimmed === "yes");
|
|
1309
|
+
};
|
|
1310
|
+
process.stdin.resume();
|
|
1311
|
+
process.stdin.on("data", handler);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async function handleAuthRoot(options) {
|
|
1315
|
+
const resolved = resolveOrg({ org: options.org });
|
|
1316
|
+
if (!resolved) {
|
|
1317
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1318
|
+
`);
|
|
1319
|
+
process.exitCode = 3;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const org = resolved.org;
|
|
1323
|
+
const wantBrowser = options.browser !== false && !options.fromStdin;
|
|
1324
|
+
if (wantBrowser) {
|
|
1325
|
+
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_usersSettings/tokens`;
|
|
1326
|
+
await openUrl(url);
|
|
1327
|
+
}
|
|
1328
|
+
const raw = options.fromStdin ? await readStdinToString() : await promptForPat();
|
|
1329
|
+
const pat = raw ? normalizePat(raw) : null;
|
|
1330
|
+
if (!pat) {
|
|
1331
|
+
process.stderr.write("No PAT provided. Aborting.\n");
|
|
1332
|
+
process.exitCode = 1;
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
let validation;
|
|
1336
|
+
try {
|
|
1337
|
+
validation = await validatePatAgainstAzdo(pat, org);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
process.stderr.write(`Could not reach Azure DevOps to validate PAT: ${err.message}
|
|
1340
|
+
`);
|
|
1341
|
+
process.exitCode = 1;
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (!validation.ok) {
|
|
1345
|
+
appendAuthAuditEvent({ event: "auth.validate.fail", org, backend: probeBackend() });
|
|
1346
|
+
process.stderr.write(`PAT validation failed (HTTP ${validation.status}). Token NOT stored.
|
|
1347
|
+
`);
|
|
1348
|
+
process.exitCode = 2;
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
appendAuthAuditEvent({
|
|
1352
|
+
event: "auth.validate.ok",
|
|
1353
|
+
org,
|
|
1354
|
+
backend: probeBackend(),
|
|
1355
|
+
masked_pat: maskedDisplay(pat)
|
|
1356
|
+
});
|
|
1357
|
+
try {
|
|
1358
|
+
const existing = await getPat(org);
|
|
1359
|
+
if (existing !== null) {
|
|
1360
|
+
const overwrite = await confirmOverwrite(org);
|
|
1361
|
+
if (!overwrite) {
|
|
1362
|
+
process.stderr.write("Aborted. Existing PAT preserved.\n");
|
|
1363
|
+
process.exitCode = 1;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
await storePat(org, pat);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1370
|
+
process.stderr.write(`${err.message}
|
|
1371
|
+
`);
|
|
1372
|
+
process.exitCode = 4;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
throw err;
|
|
1376
|
+
}
|
|
1377
|
+
process.stdout.write(`PAT stored for org ${org} in ${probeBackend()}.
|
|
1378
|
+
`);
|
|
1379
|
+
}
|
|
1380
|
+
async function handleStatus(options, org) {
|
|
1381
|
+
let backend;
|
|
1382
|
+
let value;
|
|
1383
|
+
try {
|
|
1384
|
+
backend = probeBackend();
|
|
1385
|
+
value = await getPat(org);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1388
|
+
process.stderr.write(`${err.message}
|
|
1389
|
+
`);
|
|
1390
|
+
process.exitCode = 4;
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
throw err;
|
|
1394
|
+
}
|
|
1395
|
+
const storedEvents = readAuditEvents().filter((ev) => ev.org === org && ev.event === "auth.store");
|
|
1396
|
+
const last = storedEvents[storedEvents.length - 1];
|
|
1397
|
+
const updatedAt = last?.ts ?? null;
|
|
1398
|
+
if (!value) {
|
|
1399
|
+
if (options.json) {
|
|
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}
|
|
1406
|
+
Backend: ${backend}
|
|
1407
|
+
Stored: no
|
|
1408
|
+
`);
|
|
1409
|
+
}
|
|
1410
|
+
process.exitCode = 1;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const masked = maskedDisplay(value);
|
|
1414
|
+
if (options.json) {
|
|
1415
|
+
process.stdout.write(
|
|
1416
|
+
`${JSON.stringify({ org, backend, stored: true, masked, updated_at: updatedAt })}
|
|
1417
|
+
`
|
|
1418
|
+
);
|
|
1419
|
+
} else {
|
|
1420
|
+
process.stdout.write(
|
|
1421
|
+
`Organization: ${org}
|
|
1422
|
+
Backend: ${backend}
|
|
1423
|
+
Stored: yes
|
|
1424
|
+
Identifier: ${masked}
|
|
1425
|
+
` + (updatedAt ? `Last updated: ${updatedAt}
|
|
1426
|
+
` : "")
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
async function handleLogout(options, orgFromGlobal) {
|
|
1431
|
+
if (options.all && orgFromGlobal) {
|
|
1432
|
+
process.stderr.write("--org and --all are mutually exclusive.\n");
|
|
1433
|
+
process.exitCode = 1;
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (options.all) {
|
|
1437
|
+
let orgs;
|
|
1438
|
+
try {
|
|
1439
|
+
orgs = await listOrgsWithStoredPat();
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1442
|
+
process.stderr.write(`${err.message}
|
|
1443
|
+
`);
|
|
1444
|
+
process.exitCode = 4;
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
throw err;
|
|
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}
|
|
1460
|
+
`);
|
|
1461
|
+
process.exitCode = 1;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const resolved = resolveOrg({ org: orgFromGlobal });
|
|
1467
|
+
if (!resolved) {
|
|
1468
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1469
|
+
`);
|
|
1470
|
+
process.exitCode = 3;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
const removed = await deletePat(resolved.org);
|
|
1475
|
+
if (removed) {
|
|
1476
|
+
process.stdout.write(`PAT removed for org ${resolved.org}.
|
|
1477
|
+
`);
|
|
1478
|
+
} else {
|
|
1479
|
+
process.stdout.write(`No stored PAT found for org ${resolved.org}.
|
|
1480
|
+
`);
|
|
1481
|
+
}
|
|
1482
|
+
} catch (err) {
|
|
1483
|
+
if (err instanceof CredentialStoreUnavailableError) {
|
|
1484
|
+
process.stderr.write(`${err.message}
|
|
1485
|
+
`);
|
|
1486
|
+
process.exitCode = 4;
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
throw err;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function createAuthCommand() {
|
|
1493
|
+
const command = new Command3("auth");
|
|
1494
|
+
command.description("Manage Azure DevOps Personal Access Tokens (PAT) in the OS secret vault");
|
|
1495
|
+
command.option("--org <name>", "Azure DevOps organization (flag wins over auto-detect / config)").option("--from-stdin", "read PAT from stdin instead of prompting", false).option("--no-browser", "do not open the Azure DevOps PAT page in a browser");
|
|
1496
|
+
command.action(async (options) => {
|
|
1497
|
+
await handleAuthRoot(options);
|
|
1498
|
+
});
|
|
1499
|
+
const statusCmd = command.command("status").description("Report whether a PAT is stored for the resolved org (masked, never the full value)").option("--json", "emit a JSON object", false);
|
|
1500
|
+
statusCmd.action(async (options) => {
|
|
1501
|
+
const globals = statusCmd.optsWithGlobals();
|
|
1502
|
+
const resolved = resolveOrg({ org: globals.org });
|
|
1503
|
+
if (!resolved) {
|
|
1504
|
+
process.stderr.write(`${formatResolutionError()}
|
|
1505
|
+
`);
|
|
1506
|
+
process.exitCode = 3;
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
await handleStatus(options, resolved.org);
|
|
1510
|
+
});
|
|
1511
|
+
const logoutCmd = command.command("logout").description("Remove the stored PAT for an org (or all orgs with --all)").option("--all", "remove the stored PAT for every org", false);
|
|
1512
|
+
logoutCmd.action(async (options) => {
|
|
1513
|
+
const globals = logoutCmd.optsWithGlobals();
|
|
1514
|
+
await handleLogout(options, globals.org);
|
|
1515
|
+
});
|
|
1516
|
+
return command;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/commands/config.ts
|
|
1520
|
+
import { Command as Command4 } from "commander";
|
|
999
1521
|
import { createInterface as createInterface2 } from "readline";
|
|
1000
1522
|
function formatConfigValue(value, unsetFallback = "") {
|
|
1001
1523
|
if (value === void 0) {
|
|
@@ -1055,9 +1577,9 @@ async function promptForSetting(cfg, setting, ask) {
|
|
|
1055
1577
|
`);
|
|
1056
1578
|
}
|
|
1057
1579
|
function createConfigCommand() {
|
|
1058
|
-
const config = new
|
|
1580
|
+
const config = new Command4("config");
|
|
1059
1581
|
config.description("Manage CLI settings");
|
|
1060
|
-
const set = new
|
|
1582
|
+
const set = new Command4("set");
|
|
1061
1583
|
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
|
|
1062
1584
|
try {
|
|
1063
1585
|
setConfigValue(key, value);
|
|
@@ -1078,7 +1600,7 @@ function createConfigCommand() {
|
|
|
1078
1600
|
process.exit(1);
|
|
1079
1601
|
}
|
|
1080
1602
|
});
|
|
1081
|
-
const get = new
|
|
1603
|
+
const get = new Command4("get");
|
|
1082
1604
|
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
1083
1605
|
try {
|
|
1084
1606
|
const value = getConfigValue(key);
|
|
@@ -1101,7 +1623,7 @@ function createConfigCommand() {
|
|
|
1101
1623
|
process.exit(1);
|
|
1102
1624
|
}
|
|
1103
1625
|
});
|
|
1104
|
-
const list = new
|
|
1626
|
+
const list = new Command4("list");
|
|
1105
1627
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1106
1628
|
const cfg = loadConfig();
|
|
1107
1629
|
if (options.json) {
|
|
@@ -1110,7 +1632,7 @@ function createConfigCommand() {
|
|
|
1110
1632
|
}
|
|
1111
1633
|
writeConfigList(cfg);
|
|
1112
1634
|
});
|
|
1113
|
-
const unset = new
|
|
1635
|
+
const unset = new Command4("unset");
|
|
1114
1636
|
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
1115
1637
|
try {
|
|
1116
1638
|
unsetConfigValue(key);
|
|
@@ -1127,7 +1649,7 @@ function createConfigCommand() {
|
|
|
1127
1649
|
process.exit(1);
|
|
1128
1650
|
}
|
|
1129
1651
|
});
|
|
1130
|
-
const wizard = new
|
|
1652
|
+
const wizard = new Command4("wizard");
|
|
1131
1653
|
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
1132
1654
|
if (!process.stdin.isTTY) {
|
|
1133
1655
|
process.stderr.write(
|
|
@@ -1158,9 +1680,9 @@ function createConfigCommand() {
|
|
|
1158
1680
|
}
|
|
1159
1681
|
|
|
1160
1682
|
// src/commands/set-state.ts
|
|
1161
|
-
import { Command as
|
|
1683
|
+
import { Command as Command5 } from "commander";
|
|
1162
1684
|
function createSetStateCommand() {
|
|
1163
|
-
const command = new
|
|
1685
|
+
const command = new Command5("set-state");
|
|
1164
1686
|
command.description("Change the state of a work item").argument("<id>", "work item ID").argument("<state>", 'target state (e.g., "Active", "Closed")').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
1165
1687
|
async (idStr, state, options) => {
|
|
1166
1688
|
const id = parseWorkItemId(idStr);
|
|
@@ -1168,7 +1690,7 @@ function createSetStateCommand() {
|
|
|
1168
1690
|
let context;
|
|
1169
1691
|
try {
|
|
1170
1692
|
context = resolveContext(options);
|
|
1171
|
-
const credential = await
|
|
1693
|
+
const credential = await requirePat(context.org);
|
|
1172
1694
|
const operations = [
|
|
1173
1695
|
{ op: "add", path: "/fields/System.State", value: state }
|
|
1174
1696
|
];
|
|
@@ -1196,9 +1718,9 @@ function createSetStateCommand() {
|
|
|
1196
1718
|
}
|
|
1197
1719
|
|
|
1198
1720
|
// src/commands/assign.ts
|
|
1199
|
-
import { Command as
|
|
1721
|
+
import { Command as Command6 } from "commander";
|
|
1200
1722
|
function createAssignCommand() {
|
|
1201
|
-
const command = new
|
|
1723
|
+
const command = new Command6("assign");
|
|
1202
1724
|
command.description("Assign a work item to a user, or unassign it").argument("<id>", "work item ID").argument("[name]", "user display name or email").option("--unassign", "clear the Assigned To field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
1203
1725
|
async (idStr, name, options) => {
|
|
1204
1726
|
const id = parseWorkItemId(idStr);
|
|
@@ -1218,7 +1740,7 @@ function createAssignCommand() {
|
|
|
1218
1740
|
let context;
|
|
1219
1741
|
try {
|
|
1220
1742
|
context = resolveContext(options);
|
|
1221
|
-
const credential = await
|
|
1743
|
+
const credential = await requirePat(context.org);
|
|
1222
1744
|
const value = options.unassign ? "" : name;
|
|
1223
1745
|
const operations = [
|
|
1224
1746
|
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
@@ -1248,9 +1770,9 @@ function createAssignCommand() {
|
|
|
1248
1770
|
}
|
|
1249
1771
|
|
|
1250
1772
|
// src/commands/set-field.ts
|
|
1251
|
-
import { Command as
|
|
1773
|
+
import { Command as Command7 } from "commander";
|
|
1252
1774
|
function createSetFieldCommand() {
|
|
1253
|
-
const command = new
|
|
1775
|
+
const command = new Command7("set-field");
|
|
1254
1776
|
command.description("Set any work item field by its reference name").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Title)").argument("<value>", "new value for the field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
1255
1777
|
async (idStr, field, value, options) => {
|
|
1256
1778
|
const id = parseWorkItemId(idStr);
|
|
@@ -1258,7 +1780,7 @@ function createSetFieldCommand() {
|
|
|
1258
1780
|
let context;
|
|
1259
1781
|
try {
|
|
1260
1782
|
context = resolveContext(options);
|
|
1261
|
-
const credential = await
|
|
1783
|
+
const credential = await requirePat(context.org);
|
|
1262
1784
|
const operations = [
|
|
1263
1785
|
{ op: "add", path: `/fields/${field}`, value }
|
|
1264
1786
|
];
|
|
@@ -1286,9 +1808,9 @@ function createSetFieldCommand() {
|
|
|
1286
1808
|
}
|
|
1287
1809
|
|
|
1288
1810
|
// src/commands/get-md-field.ts
|
|
1289
|
-
import { Command as
|
|
1811
|
+
import { Command as Command8 } from "commander";
|
|
1290
1812
|
function createGetMdFieldCommand() {
|
|
1291
|
-
const command = new
|
|
1813
|
+
const command = new Command8("get-md-field");
|
|
1292
1814
|
command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
|
|
1293
1815
|
async (idStr, field, options) => {
|
|
1294
1816
|
const id = parseWorkItemId(idStr);
|
|
@@ -1296,7 +1818,7 @@ function createGetMdFieldCommand() {
|
|
|
1296
1818
|
let context;
|
|
1297
1819
|
try {
|
|
1298
1820
|
context = resolveContext(options);
|
|
1299
|
-
const credential = await
|
|
1821
|
+
const credential = await requirePat(context.org);
|
|
1300
1822
|
const value = await getWorkItemFieldValue(context, id, credential.pat, field);
|
|
1301
1823
|
if (value === null) {
|
|
1302
1824
|
process.stdout.write("\n");
|
|
@@ -1313,7 +1835,7 @@ function createGetMdFieldCommand() {
|
|
|
1313
1835
|
|
|
1314
1836
|
// src/commands/set-md-field.ts
|
|
1315
1837
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1316
|
-
import { Command as
|
|
1838
|
+
import { Command as Command9 } from "commander";
|
|
1317
1839
|
function fail(message) {
|
|
1318
1840
|
process.stderr.write(`Error: ${message}
|
|
1319
1841
|
`);
|
|
@@ -1375,7 +1897,7 @@ function formatOutput(result, options, field) {
|
|
|
1375
1897
|
}
|
|
1376
1898
|
}
|
|
1377
1899
|
function createSetMdFieldCommand() {
|
|
1378
|
-
const command = new
|
|
1900
|
+
const command = new Command9("set-md-field");
|
|
1379
1901
|
command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
|
|
1380
1902
|
async (idStr, field, inlineContent, options) => {
|
|
1381
1903
|
const id = parseWorkItemId(idStr);
|
|
@@ -1384,7 +1906,7 @@ function createSetMdFieldCommand() {
|
|
|
1384
1906
|
let context;
|
|
1385
1907
|
try {
|
|
1386
1908
|
context = resolveContext(options);
|
|
1387
|
-
const credential = await
|
|
1909
|
+
const credential = await requirePat(context.org);
|
|
1388
1910
|
const operations = [
|
|
1389
1911
|
{ op: "add", path: `/fields/${field}`, value: content },
|
|
1390
1912
|
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
@@ -1401,7 +1923,7 @@ function createSetMdFieldCommand() {
|
|
|
1401
1923
|
|
|
1402
1924
|
// src/commands/upsert.ts
|
|
1403
1925
|
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1404
|
-
import { Command as
|
|
1926
|
+
import { Command as Command10 } from "commander";
|
|
1405
1927
|
|
|
1406
1928
|
// src/services/task-document.ts
|
|
1407
1929
|
var FIELD_ALIASES = /* @__PURE__ */ new Map([
|
|
@@ -1685,7 +2207,7 @@ function handleUpsertError(err, id, context) {
|
|
|
1685
2207
|
process.exit(1);
|
|
1686
2208
|
}
|
|
1687
2209
|
function createUpsertCommand() {
|
|
1688
|
-
const command = new
|
|
2210
|
+
const command = new Command10("upsert");
|
|
1689
2211
|
command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
1690
2212
|
validateOrgProjectPair(options);
|
|
1691
2213
|
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
@@ -1700,7 +2222,7 @@ function createUpsertCommand() {
|
|
|
1700
2222
|
ensureTitleForCreate(document.fields);
|
|
1701
2223
|
}
|
|
1702
2224
|
const operations = toPatchOperations(document.fields, action);
|
|
1703
|
-
const credential = await
|
|
2225
|
+
const credential = await requirePat(context.org);
|
|
1704
2226
|
let writeResult;
|
|
1705
2227
|
if (action === "created") {
|
|
1706
2228
|
writeResult = await createWorkItem(context, createType, credential.pat, operations);
|
|
@@ -1726,7 +2248,7 @@ function createUpsertCommand() {
|
|
|
1726
2248
|
}
|
|
1727
2249
|
|
|
1728
2250
|
// src/commands/list-fields.ts
|
|
1729
|
-
import { Command as
|
|
2251
|
+
import { Command as Command11 } from "commander";
|
|
1730
2252
|
function stringifyValue(value) {
|
|
1731
2253
|
if (value === null || value === void 0) return "";
|
|
1732
2254
|
if (typeof value === "object") return JSON.stringify(value);
|
|
@@ -1758,7 +2280,7 @@ function formatFieldList(fields) {
|
|
|
1758
2280
|
}).join("\n");
|
|
1759
2281
|
}
|
|
1760
2282
|
function createListFieldsCommand() {
|
|
1761
|
-
const command = new
|
|
2283
|
+
const command = new Command11("list-fields");
|
|
1762
2284
|
command.description("List all fields of an Azure DevOps work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
1763
2285
|
async (idStr, options) => {
|
|
1764
2286
|
const id = parseWorkItemId(idStr);
|
|
@@ -1766,7 +2288,7 @@ function createListFieldsCommand() {
|
|
|
1766
2288
|
let context;
|
|
1767
2289
|
try {
|
|
1768
2290
|
context = resolveContext(options);
|
|
1769
|
-
const credential = await
|
|
2291
|
+
const credential = await requirePat(context.org);
|
|
1770
2292
|
const fields = await getWorkItemFields(context, id, credential.pat);
|
|
1771
2293
|
if (options.json) {
|
|
1772
2294
|
process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
|
|
@@ -1785,7 +2307,7 @@ function createListFieldsCommand() {
|
|
|
1785
2307
|
}
|
|
1786
2308
|
|
|
1787
2309
|
// src/commands/pr.ts
|
|
1788
|
-
import { Command as
|
|
2310
|
+
import { Command as Command12 } from "commander";
|
|
1789
2311
|
|
|
1790
2312
|
// src/services/pr-client.ts
|
|
1791
2313
|
function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
|
|
@@ -2015,7 +2537,7 @@ async function resolvePrCommandContext(options) {
|
|
|
2015
2537
|
const context = resolveContext(options);
|
|
2016
2538
|
const repo = detectRepoName();
|
|
2017
2539
|
const branch = getCurrentBranch();
|
|
2018
|
-
const credential = await
|
|
2540
|
+
const credential = await requirePat(context.org);
|
|
2019
2541
|
return {
|
|
2020
2542
|
context,
|
|
2021
2543
|
repo,
|
|
@@ -2024,7 +2546,7 @@ async function resolvePrCommandContext(options) {
|
|
|
2024
2546
|
};
|
|
2025
2547
|
}
|
|
2026
2548
|
function createPrStatusCommand() {
|
|
2027
|
-
const command = new
|
|
2549
|
+
const command = new Command12("status");
|
|
2028
2550
|
command.description("Check pull requests for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
2029
2551
|
validateOrgProjectPair(options);
|
|
2030
2552
|
let context;
|
|
@@ -2059,7 +2581,7 @@ function createPrStatusCommand() {
|
|
|
2059
2581
|
return command;
|
|
2060
2582
|
}
|
|
2061
2583
|
function createPrOpenCommand() {
|
|
2062
|
-
const command = new
|
|
2584
|
+
const command = new Command12("open");
|
|
2063
2585
|
command.description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option("--description <description>", "pull request description").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
2064
2586
|
validateOrgProjectPair(options);
|
|
2065
2587
|
const title = options.title?.trim();
|
|
@@ -2112,7 +2634,7 @@ ${result.pullRequest.url}
|
|
|
2112
2634
|
return command;
|
|
2113
2635
|
}
|
|
2114
2636
|
function createPrCommentsCommand() {
|
|
2115
|
-
const command = new
|
|
2637
|
+
const command = new Command12("comments");
|
|
2116
2638
|
command.description("List active pull request comments for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
2117
2639
|
validateOrgProjectPair(options);
|
|
2118
2640
|
let context;
|
|
@@ -2151,7 +2673,7 @@ function createPrCommentsCommand() {
|
|
|
2151
2673
|
return command;
|
|
2152
2674
|
}
|
|
2153
2675
|
function createPrCommand() {
|
|
2154
|
-
const command = new
|
|
2676
|
+
const command = new Command12("pr");
|
|
2155
2677
|
command.description("Manage Azure DevOps pull requests");
|
|
2156
2678
|
command.addCommand(createPrStatusCommand());
|
|
2157
2679
|
command.addCommand(createPrOpenCommand());
|
|
@@ -2160,7 +2682,7 @@ function createPrCommand() {
|
|
|
2160
2682
|
}
|
|
2161
2683
|
|
|
2162
2684
|
// src/commands/comments.ts
|
|
2163
|
-
import { Command as
|
|
2685
|
+
import { Command as Command13 } from "commander";
|
|
2164
2686
|
function writeError2(message) {
|
|
2165
2687
|
process.stderr.write(`Error: ${message}
|
|
2166
2688
|
`);
|
|
@@ -2180,14 +2702,14 @@ function formatComments(result, convertMarkdown) {
|
|
|
2180
2702
|
return lines.join("\n");
|
|
2181
2703
|
}
|
|
2182
2704
|
function createCommentsListCommand() {
|
|
2183
|
-
const command = new
|
|
2705
|
+
const command = new Command13("list");
|
|
2184
2706
|
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "convert HTML comment bodies to markdown").action(async (idStr, options) => {
|
|
2185
2707
|
validateOrgProjectPair(options);
|
|
2186
2708
|
const id = parseWorkItemId(idStr);
|
|
2187
2709
|
let context;
|
|
2188
2710
|
try {
|
|
2189
2711
|
context = resolveContext(options);
|
|
2190
|
-
const credential = await
|
|
2712
|
+
const credential = await requirePat(context.org);
|
|
2191
2713
|
const result = await listWorkItemComments(context, id, credential.pat);
|
|
2192
2714
|
if (options.json) {
|
|
2193
2715
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -2208,7 +2730,7 @@ function createCommentsListCommand() {
|
|
|
2208
2730
|
return command;
|
|
2209
2731
|
}
|
|
2210
2732
|
function createCommentsAddCommand() {
|
|
2211
|
-
const command = new
|
|
2733
|
+
const command = new Command13("add");
|
|
2212
2734
|
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, text, options) => {
|
|
2213
2735
|
validateOrgProjectPair(options);
|
|
2214
2736
|
const id = parseWorkItemId(idStr);
|
|
@@ -2218,7 +2740,7 @@ function createCommentsAddCommand() {
|
|
|
2218
2740
|
let context;
|
|
2219
2741
|
try {
|
|
2220
2742
|
context = resolveContext(options);
|
|
2221
|
-
const credential = await
|
|
2743
|
+
const credential = await requirePat(context.org);
|
|
2222
2744
|
const format = options.markdown === true ? "markdown" : "html";
|
|
2223
2745
|
const result = await addWorkItemComment(context, id, credential.pat, text, format);
|
|
2224
2746
|
if (options.json) {
|
|
@@ -2235,7 +2757,7 @@ function createCommentsAddCommand() {
|
|
|
2235
2757
|
return command;
|
|
2236
2758
|
}
|
|
2237
2759
|
function createCommentsCommand() {
|
|
2238
|
-
const command = new
|
|
2760
|
+
const command = new Command13("comments");
|
|
2239
2761
|
command.description("Manage Azure DevOps work item comments");
|
|
2240
2762
|
command.addCommand(createCommentsListCommand());
|
|
2241
2763
|
command.addCommand(createCommentsAddCommand());
|
|
@@ -2243,12 +2765,12 @@ function createCommentsCommand() {
|
|
|
2243
2765
|
}
|
|
2244
2766
|
|
|
2245
2767
|
// src/commands/download-attachment.ts
|
|
2246
|
-
import { Command as
|
|
2768
|
+
import { Command as Command14 } from "commander";
|
|
2247
2769
|
import { writeFile } from "fs/promises";
|
|
2248
2770
|
import { existsSync as existsSync4 } from "fs";
|
|
2249
2771
|
import { join as join2 } from "path";
|
|
2250
2772
|
function createDownloadAttachmentCommand() {
|
|
2251
|
-
const command = new
|
|
2773
|
+
const command = new Command14("download-attachment");
|
|
2252
2774
|
command.description("Download an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to download").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--output <dir>", "target directory for the downloaded file").action(
|
|
2253
2775
|
async (idStr, filename, options) => {
|
|
2254
2776
|
const id = parseWorkItemId(idStr);
|
|
@@ -2256,7 +2778,7 @@ function createDownloadAttachmentCommand() {
|
|
|
2256
2778
|
let context;
|
|
2257
2779
|
try {
|
|
2258
2780
|
context = resolveContext(options);
|
|
2259
|
-
const credential = await
|
|
2781
|
+
const credential = await requirePat(context.org);
|
|
2260
2782
|
const outputDir = options.output ?? ".";
|
|
2261
2783
|
if (!existsSync4(outputDir)) {
|
|
2262
2784
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
@@ -2290,9 +2812,10 @@ function createDownloadAttachmentCommand() {
|
|
|
2290
2812
|
}
|
|
2291
2813
|
|
|
2292
2814
|
// src/index.ts
|
|
2293
|
-
var program = new
|
|
2815
|
+
var program = new Command15();
|
|
2294
2816
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
2295
2817
|
program.addCommand(createGetItemCommand());
|
|
2818
|
+
program.addCommand(createAuthCommand());
|
|
2296
2819
|
program.addCommand(createClearPatCommand());
|
|
2297
2820
|
program.addCommand(createConfigCommand());
|
|
2298
2821
|
program.addCommand(createSetStateCommand());
|