azdo-cli 0.10.0-develop.210 → 0.10.0-develop.232
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 +708 -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;
|
|
@@ -417,7 +693,8 @@ async function promptForPat() {
|
|
|
417
693
|
return new Promise((resolve2) => {
|
|
418
694
|
const rl = createInterface({
|
|
419
695
|
input: process.stdin,
|
|
420
|
-
|
|
696
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
697
|
+
output: null
|
|
421
698
|
});
|
|
422
699
|
process.stderr.write(PAT_PROMPT);
|
|
423
700
|
process.stdin.setRawMode(true);
|
|
@@ -473,12 +750,12 @@ function findDotEnvPat(startDir = process.cwd()) {
|
|
|
473
750
|
}
|
|
474
751
|
return null;
|
|
475
752
|
}
|
|
476
|
-
async function resolvePat(
|
|
753
|
+
async function resolvePat(org) {
|
|
477
754
|
const envPat = process.env.AZDO_PAT;
|
|
478
|
-
if (envPat) {
|
|
755
|
+
if (envPat && envPat.length > 0) {
|
|
479
756
|
return { pat: envPat, source: "env" };
|
|
480
757
|
}
|
|
481
|
-
const storedPat = await getPat();
|
|
758
|
+
const storedPat = await getPat(org);
|
|
482
759
|
if (storedPat !== null) {
|
|
483
760
|
return { pat: storedPat, source: "credential-store" };
|
|
484
761
|
}
|
|
@@ -486,21 +763,34 @@ async function resolvePat(promptFn = promptForPat) {
|
|
|
486
763
|
if (dotEnvPat !== null) {
|
|
487
764
|
return { pat: dotEnvPat, source: "env" };
|
|
488
765
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
process.stderr.write("Warning: Could not save PAT to credential store. You may need to enter it again next time.\n");
|
|
496
|
-
}
|
|
497
|
-
return { pat: normalizedPat, source: "prompt" };
|
|
498
|
-
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
async function requirePat(org) {
|
|
769
|
+
const cred = await resolvePat(org);
|
|
770
|
+
if (cred !== null) {
|
|
771
|
+
return cred;
|
|
499
772
|
}
|
|
500
773
|
throw new Error(
|
|
501
|
-
"
|
|
774
|
+
`No PAT available for org "${org}". Set AZDO_PAT environment variable or run \`azdo auth --org ${org}\`.`
|
|
502
775
|
);
|
|
503
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
|
+
}
|
|
504
794
|
|
|
505
795
|
// src/services/git-remote.ts
|
|
506
796
|
import { execSync } from "child_process";
|
|
@@ -572,122 +862,57 @@ function getCurrentBranch() {
|
|
|
572
862
|
return branch;
|
|
573
863
|
}
|
|
574
864
|
|
|
575
|
-
// src/services/
|
|
576
|
-
|
|
577
|
-
import path from "path";
|
|
578
|
-
import os from "os";
|
|
579
|
-
var SETTINGS = [
|
|
580
|
-
{
|
|
581
|
-
key: "org",
|
|
582
|
-
description: "Azure DevOps organization name",
|
|
583
|
-
type: "string",
|
|
584
|
-
example: "mycompany",
|
|
585
|
-
required: true
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
key: "project",
|
|
589
|
-
description: "Azure DevOps project name",
|
|
590
|
-
type: "string",
|
|
591
|
-
example: "MyProject",
|
|
592
|
-
required: true
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
key: "fields",
|
|
596
|
-
description: "Extra work item fields to include (comma-separated reference names)",
|
|
597
|
-
type: "string[]",
|
|
598
|
-
example: "System.Tags,Custom.Priority",
|
|
599
|
-
required: false
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
key: "markdown",
|
|
603
|
-
description: "Convert rich text fields to markdown on display",
|
|
604
|
-
type: "boolean",
|
|
605
|
-
example: "true",
|
|
606
|
-
required: false
|
|
607
|
-
}
|
|
608
|
-
];
|
|
609
|
-
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
610
|
-
function getConfigPath() {
|
|
611
|
-
return path.join(os.homedir(), ".azdo", "config.json");
|
|
612
|
-
}
|
|
613
|
-
function loadConfig() {
|
|
614
|
-
const configPath = getConfigPath();
|
|
615
|
-
let raw;
|
|
616
|
-
try {
|
|
617
|
-
raw = fs.readFileSync(configPath, "utf-8");
|
|
618
|
-
} catch (err) {
|
|
619
|
-
if (err.code === "ENOENT") {
|
|
620
|
-
return {};
|
|
621
|
-
}
|
|
622
|
-
throw err;
|
|
623
|
-
}
|
|
865
|
+
// src/services/org-resolver.ts
|
|
866
|
+
function defaultDetectFromGit() {
|
|
624
867
|
try {
|
|
625
|
-
return
|
|
868
|
+
return detectAzdoContext().org ?? null;
|
|
626
869
|
} catch {
|
|
627
|
-
|
|
628
|
-
`);
|
|
629
|
-
return {};
|
|
870
|
+
return null;
|
|
630
871
|
}
|
|
631
872
|
}
|
|
632
|
-
function
|
|
633
|
-
|
|
634
|
-
const dir = path.dirname(configPath);
|
|
635
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
636
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
873
|
+
function defaultReadConfig() {
|
|
874
|
+
return loadConfig();
|
|
637
875
|
}
|
|
638
|
-
function
|
|
639
|
-
if (
|
|
640
|
-
|
|
876
|
+
function resolveOrg(options) {
|
|
877
|
+
if (options.org && options.org.length > 0) {
|
|
878
|
+
return { org: options.org, source: "flag" };
|
|
641
879
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const config = loadConfig();
|
|
646
|
-
return config[key];
|
|
647
|
-
}
|
|
648
|
-
function setConfigValue(key, value) {
|
|
649
|
-
validateKey(key);
|
|
650
|
-
const config = loadConfig();
|
|
651
|
-
if (value === "") {
|
|
652
|
-
delete config[key];
|
|
653
|
-
} else if (key === "markdown") {
|
|
654
|
-
if (value !== "true" && value !== "false") {
|
|
655
|
-
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
656
|
-
}
|
|
657
|
-
config.markdown = value === "true";
|
|
658
|
-
} else if (key === "fields") {
|
|
659
|
-
config.fields = value.split(",").map((s) => s.trim());
|
|
660
|
-
} else {
|
|
661
|
-
config[key] = value;
|
|
880
|
+
const gitOrg = (options.detectFromGit ?? defaultDetectFromGit)();
|
|
881
|
+
if (gitOrg && gitOrg.length > 0) {
|
|
882
|
+
return { org: gitOrg, source: "git" };
|
|
662
883
|
}
|
|
663
|
-
|
|
884
|
+
const configOrg = (options.readConfig ?? defaultReadConfig)().org;
|
|
885
|
+
if (configOrg && configOrg.length > 0) {
|
|
886
|
+
return { org: configOrg, source: "config" };
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
664
889
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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");
|
|
670
897
|
}
|
|
671
898
|
|
|
672
899
|
// src/services/context.ts
|
|
673
900
|
function resolveContext(options) {
|
|
674
|
-
|
|
675
|
-
return { org: options.org, project: options.project };
|
|
676
|
-
}
|
|
677
|
-
const config = loadConfig();
|
|
678
|
-
if (config.org && config.project) {
|
|
679
|
-
return { org: config.org, project: config.project };
|
|
680
|
-
}
|
|
901
|
+
const resolvedOrg = resolveOrg({ org: options.org });
|
|
681
902
|
let gitContext = null;
|
|
682
903
|
try {
|
|
683
904
|
gitContext = detectAzdoContext();
|
|
684
905
|
} catch {
|
|
685
906
|
}
|
|
686
|
-
const
|
|
687
|
-
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;
|
|
688
910
|
if (org && project) {
|
|
689
911
|
return { org, project };
|
|
690
912
|
}
|
|
913
|
+
if (!org) {
|
|
914
|
+
throw new Error(formatResolutionError());
|
|
915
|
+
}
|
|
691
916
|
throw new Error(
|
|
692
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".'
|
|
693
918
|
);
|
|
@@ -963,7 +1188,7 @@ function createGetItemCommand() {
|
|
|
963
1188
|
let context;
|
|
964
1189
|
try {
|
|
965
1190
|
context = resolveContext(options);
|
|
966
|
-
const credential = await
|
|
1191
|
+
const credential = await requirePat(context.org);
|
|
967
1192
|
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
968
1193
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
969
1194
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
@@ -981,19 +1206,318 @@ function createGetItemCommand() {
|
|
|
981
1206
|
import { Command as Command2 } from "commander";
|
|
982
1207
|
function createClearPatCommand() {
|
|
983
1208
|
const command = new Command2("clear-pat");
|
|
984
|
-
command.description("Remove
|
|
985
|
-
|
|
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);
|
|
986
1219
|
if (deleted) {
|
|
987
|
-
process.stdout.write(
|
|
1220
|
+
process.stdout.write(`PAT removed for org ${resolved.org}.
|
|
1221
|
+
`);
|
|
988
1222
|
} else {
|
|
989
|
-
process.stdout.write(
|
|
1223
|
+
process.stdout.write(`No stored PAT found for org ${resolved.org}.
|
|
1224
|
+
`);
|
|
990
1225
|
}
|
|
991
1226
|
});
|
|
992
1227
|
return command;
|
|
993
1228
|
}
|
|
994
1229
|
|
|
995
|
-
// src/commands/
|
|
1230
|
+
// src/commands/auth.ts
|
|
996
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";
|
|
997
1521
|
import { createInterface as createInterface2 } from "readline";
|
|
998
1522
|
function formatConfigValue(value, unsetFallback = "") {
|
|
999
1523
|
if (value === void 0) {
|
|
@@ -1053,9 +1577,9 @@ async function promptForSetting(cfg, setting, ask) {
|
|
|
1053
1577
|
`);
|
|
1054
1578
|
}
|
|
1055
1579
|
function createConfigCommand() {
|
|
1056
|
-
const config = new
|
|
1580
|
+
const config = new Command4("config");
|
|
1057
1581
|
config.description("Manage CLI settings");
|
|
1058
|
-
const set = new
|
|
1582
|
+
const set = new Command4("set");
|
|
1059
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) => {
|
|
1060
1584
|
try {
|
|
1061
1585
|
setConfigValue(key, value);
|
|
@@ -1076,7 +1600,7 @@ function createConfigCommand() {
|
|
|
1076
1600
|
process.exit(1);
|
|
1077
1601
|
}
|
|
1078
1602
|
});
|
|
1079
|
-
const get = new
|
|
1603
|
+
const get = new Command4("get");
|
|
1080
1604
|
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
1081
1605
|
try {
|
|
1082
1606
|
const value = getConfigValue(key);
|
|
@@ -1099,7 +1623,7 @@ function createConfigCommand() {
|
|
|
1099
1623
|
process.exit(1);
|
|
1100
1624
|
}
|
|
1101
1625
|
});
|
|
1102
|
-
const list = new
|
|
1626
|
+
const list = new Command4("list");
|
|
1103
1627
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1104
1628
|
const cfg = loadConfig();
|
|
1105
1629
|
if (options.json) {
|
|
@@ -1108,7 +1632,7 @@ function createConfigCommand() {
|
|
|
1108
1632
|
}
|
|
1109
1633
|
writeConfigList(cfg);
|
|
1110
1634
|
});
|
|
1111
|
-
const unset = new
|
|
1635
|
+
const unset = new Command4("unset");
|
|
1112
1636
|
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
1113
1637
|
try {
|
|
1114
1638
|
unsetConfigValue(key);
|
|
@@ -1125,7 +1649,7 @@ function createConfigCommand() {
|
|
|
1125
1649
|
process.exit(1);
|
|
1126
1650
|
}
|
|
1127
1651
|
});
|
|
1128
|
-
const wizard = new
|
|
1652
|
+
const wizard = new Command4("wizard");
|
|
1129
1653
|
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
1130
1654
|
if (!process.stdin.isTTY) {
|
|
1131
1655
|
process.stderr.write(
|
|
@@ -1156,9 +1680,9 @@ function createConfigCommand() {
|
|
|
1156
1680
|
}
|
|
1157
1681
|
|
|
1158
1682
|
// src/commands/set-state.ts
|
|
1159
|
-
import { Command as
|
|
1683
|
+
import { Command as Command5 } from "commander";
|
|
1160
1684
|
function createSetStateCommand() {
|
|
1161
|
-
const command = new
|
|
1685
|
+
const command = new Command5("set-state");
|
|
1162
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(
|
|
1163
1687
|
async (idStr, state, options) => {
|
|
1164
1688
|
const id = parseWorkItemId(idStr);
|
|
@@ -1166,7 +1690,7 @@ function createSetStateCommand() {
|
|
|
1166
1690
|
let context;
|
|
1167
1691
|
try {
|
|
1168
1692
|
context = resolveContext(options);
|
|
1169
|
-
const credential = await
|
|
1693
|
+
const credential = await requirePat(context.org);
|
|
1170
1694
|
const operations = [
|
|
1171
1695
|
{ op: "add", path: "/fields/System.State", value: state }
|
|
1172
1696
|
];
|
|
@@ -1194,9 +1718,9 @@ function createSetStateCommand() {
|
|
|
1194
1718
|
}
|
|
1195
1719
|
|
|
1196
1720
|
// src/commands/assign.ts
|
|
1197
|
-
import { Command as
|
|
1721
|
+
import { Command as Command6 } from "commander";
|
|
1198
1722
|
function createAssignCommand() {
|
|
1199
|
-
const command = new
|
|
1723
|
+
const command = new Command6("assign");
|
|
1200
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(
|
|
1201
1725
|
async (idStr, name, options) => {
|
|
1202
1726
|
const id = parseWorkItemId(idStr);
|
|
@@ -1216,7 +1740,7 @@ function createAssignCommand() {
|
|
|
1216
1740
|
let context;
|
|
1217
1741
|
try {
|
|
1218
1742
|
context = resolveContext(options);
|
|
1219
|
-
const credential = await
|
|
1743
|
+
const credential = await requirePat(context.org);
|
|
1220
1744
|
const value = options.unassign ? "" : name;
|
|
1221
1745
|
const operations = [
|
|
1222
1746
|
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
@@ -1246,9 +1770,9 @@ function createAssignCommand() {
|
|
|
1246
1770
|
}
|
|
1247
1771
|
|
|
1248
1772
|
// src/commands/set-field.ts
|
|
1249
|
-
import { Command as
|
|
1773
|
+
import { Command as Command7 } from "commander";
|
|
1250
1774
|
function createSetFieldCommand() {
|
|
1251
|
-
const command = new
|
|
1775
|
+
const command = new Command7("set-field");
|
|
1252
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(
|
|
1253
1777
|
async (idStr, field, value, options) => {
|
|
1254
1778
|
const id = parseWorkItemId(idStr);
|
|
@@ -1256,7 +1780,7 @@ function createSetFieldCommand() {
|
|
|
1256
1780
|
let context;
|
|
1257
1781
|
try {
|
|
1258
1782
|
context = resolveContext(options);
|
|
1259
|
-
const credential = await
|
|
1783
|
+
const credential = await requirePat(context.org);
|
|
1260
1784
|
const operations = [
|
|
1261
1785
|
{ op: "add", path: `/fields/${field}`, value }
|
|
1262
1786
|
];
|
|
@@ -1284,9 +1808,9 @@ function createSetFieldCommand() {
|
|
|
1284
1808
|
}
|
|
1285
1809
|
|
|
1286
1810
|
// src/commands/get-md-field.ts
|
|
1287
|
-
import { Command as
|
|
1811
|
+
import { Command as Command8 } from "commander";
|
|
1288
1812
|
function createGetMdFieldCommand() {
|
|
1289
|
-
const command = new
|
|
1813
|
+
const command = new Command8("get-md-field");
|
|
1290
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(
|
|
1291
1815
|
async (idStr, field, options) => {
|
|
1292
1816
|
const id = parseWorkItemId(idStr);
|
|
@@ -1294,7 +1818,7 @@ function createGetMdFieldCommand() {
|
|
|
1294
1818
|
let context;
|
|
1295
1819
|
try {
|
|
1296
1820
|
context = resolveContext(options);
|
|
1297
|
-
const credential = await
|
|
1821
|
+
const credential = await requirePat(context.org);
|
|
1298
1822
|
const value = await getWorkItemFieldValue(context, id, credential.pat, field);
|
|
1299
1823
|
if (value === null) {
|
|
1300
1824
|
process.stdout.write("\n");
|
|
@@ -1311,7 +1835,7 @@ function createGetMdFieldCommand() {
|
|
|
1311
1835
|
|
|
1312
1836
|
// src/commands/set-md-field.ts
|
|
1313
1837
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1314
|
-
import { Command as
|
|
1838
|
+
import { Command as Command9 } from "commander";
|
|
1315
1839
|
function fail(message) {
|
|
1316
1840
|
process.stderr.write(`Error: ${message}
|
|
1317
1841
|
`);
|
|
@@ -1373,7 +1897,7 @@ function formatOutput(result, options, field) {
|
|
|
1373
1897
|
}
|
|
1374
1898
|
}
|
|
1375
1899
|
function createSetMdFieldCommand() {
|
|
1376
|
-
const command = new
|
|
1900
|
+
const command = new Command9("set-md-field");
|
|
1377
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(
|
|
1378
1902
|
async (idStr, field, inlineContent, options) => {
|
|
1379
1903
|
const id = parseWorkItemId(idStr);
|
|
@@ -1382,7 +1906,7 @@ function createSetMdFieldCommand() {
|
|
|
1382
1906
|
let context;
|
|
1383
1907
|
try {
|
|
1384
1908
|
context = resolveContext(options);
|
|
1385
|
-
const credential = await
|
|
1909
|
+
const credential = await requirePat(context.org);
|
|
1386
1910
|
const operations = [
|
|
1387
1911
|
{ op: "add", path: `/fields/${field}`, value: content },
|
|
1388
1912
|
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
@@ -1399,7 +1923,7 @@ function createSetMdFieldCommand() {
|
|
|
1399
1923
|
|
|
1400
1924
|
// src/commands/upsert.ts
|
|
1401
1925
|
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1402
|
-
import { Command as
|
|
1926
|
+
import { Command as Command10 } from "commander";
|
|
1403
1927
|
|
|
1404
1928
|
// src/services/task-document.ts
|
|
1405
1929
|
var FIELD_ALIASES = /* @__PURE__ */ new Map([
|
|
@@ -1683,7 +2207,7 @@ function handleUpsertError(err, id, context) {
|
|
|
1683
2207
|
process.exit(1);
|
|
1684
2208
|
}
|
|
1685
2209
|
function createUpsertCommand() {
|
|
1686
|
-
const command = new
|
|
2210
|
+
const command = new Command10("upsert");
|
|
1687
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) => {
|
|
1688
2212
|
validateOrgProjectPair(options);
|
|
1689
2213
|
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
@@ -1698,7 +2222,7 @@ function createUpsertCommand() {
|
|
|
1698
2222
|
ensureTitleForCreate(document.fields);
|
|
1699
2223
|
}
|
|
1700
2224
|
const operations = toPatchOperations(document.fields, action);
|
|
1701
|
-
const credential = await
|
|
2225
|
+
const credential = await requirePat(context.org);
|
|
1702
2226
|
let writeResult;
|
|
1703
2227
|
if (action === "created") {
|
|
1704
2228
|
writeResult = await createWorkItem(context, createType, credential.pat, operations);
|
|
@@ -1724,7 +2248,7 @@ function createUpsertCommand() {
|
|
|
1724
2248
|
}
|
|
1725
2249
|
|
|
1726
2250
|
// src/commands/list-fields.ts
|
|
1727
|
-
import { Command as
|
|
2251
|
+
import { Command as Command11 } from "commander";
|
|
1728
2252
|
function stringifyValue(value) {
|
|
1729
2253
|
if (value === null || value === void 0) return "";
|
|
1730
2254
|
if (typeof value === "object") return JSON.stringify(value);
|
|
@@ -1756,7 +2280,7 @@ function formatFieldList(fields) {
|
|
|
1756
2280
|
}).join("\n");
|
|
1757
2281
|
}
|
|
1758
2282
|
function createListFieldsCommand() {
|
|
1759
|
-
const command = new
|
|
2283
|
+
const command = new Command11("list-fields");
|
|
1760
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(
|
|
1761
2285
|
async (idStr, options) => {
|
|
1762
2286
|
const id = parseWorkItemId(idStr);
|
|
@@ -1764,7 +2288,7 @@ function createListFieldsCommand() {
|
|
|
1764
2288
|
let context;
|
|
1765
2289
|
try {
|
|
1766
2290
|
context = resolveContext(options);
|
|
1767
|
-
const credential = await
|
|
2291
|
+
const credential = await requirePat(context.org);
|
|
1768
2292
|
const fields = await getWorkItemFields(context, id, credential.pat);
|
|
1769
2293
|
if (options.json) {
|
|
1770
2294
|
process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
|
|
@@ -1783,7 +2307,7 @@ function createListFieldsCommand() {
|
|
|
1783
2307
|
}
|
|
1784
2308
|
|
|
1785
2309
|
// src/commands/pr.ts
|
|
1786
|
-
import { Command as
|
|
2310
|
+
import { Command as Command12 } from "commander";
|
|
1787
2311
|
|
|
1788
2312
|
// src/services/pr-client.ts
|
|
1789
2313
|
function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
|
|
@@ -2013,7 +2537,7 @@ async function resolvePrCommandContext(options) {
|
|
|
2013
2537
|
const context = resolveContext(options);
|
|
2014
2538
|
const repo = detectRepoName();
|
|
2015
2539
|
const branch = getCurrentBranch();
|
|
2016
|
-
const credential = await
|
|
2540
|
+
const credential = await requirePat(context.org);
|
|
2017
2541
|
return {
|
|
2018
2542
|
context,
|
|
2019
2543
|
repo,
|
|
@@ -2022,7 +2546,7 @@ async function resolvePrCommandContext(options) {
|
|
|
2022
2546
|
};
|
|
2023
2547
|
}
|
|
2024
2548
|
function createPrStatusCommand() {
|
|
2025
|
-
const command = new
|
|
2549
|
+
const command = new Command12("status");
|
|
2026
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) => {
|
|
2027
2551
|
validateOrgProjectPair(options);
|
|
2028
2552
|
let context;
|
|
@@ -2057,7 +2581,7 @@ function createPrStatusCommand() {
|
|
|
2057
2581
|
return command;
|
|
2058
2582
|
}
|
|
2059
2583
|
function createPrOpenCommand() {
|
|
2060
|
-
const command = new
|
|
2584
|
+
const command = new Command12("open");
|
|
2061
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) => {
|
|
2062
2586
|
validateOrgProjectPair(options);
|
|
2063
2587
|
const title = options.title?.trim();
|
|
@@ -2110,7 +2634,7 @@ ${result.pullRequest.url}
|
|
|
2110
2634
|
return command;
|
|
2111
2635
|
}
|
|
2112
2636
|
function createPrCommentsCommand() {
|
|
2113
|
-
const command = new
|
|
2637
|
+
const command = new Command12("comments");
|
|
2114
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) => {
|
|
2115
2639
|
validateOrgProjectPair(options);
|
|
2116
2640
|
let context;
|
|
@@ -2149,7 +2673,7 @@ function createPrCommentsCommand() {
|
|
|
2149
2673
|
return command;
|
|
2150
2674
|
}
|
|
2151
2675
|
function createPrCommand() {
|
|
2152
|
-
const command = new
|
|
2676
|
+
const command = new Command12("pr");
|
|
2153
2677
|
command.description("Manage Azure DevOps pull requests");
|
|
2154
2678
|
command.addCommand(createPrStatusCommand());
|
|
2155
2679
|
command.addCommand(createPrOpenCommand());
|
|
@@ -2158,7 +2682,7 @@ function createPrCommand() {
|
|
|
2158
2682
|
}
|
|
2159
2683
|
|
|
2160
2684
|
// src/commands/comments.ts
|
|
2161
|
-
import { Command as
|
|
2685
|
+
import { Command as Command13 } from "commander";
|
|
2162
2686
|
function writeError2(message) {
|
|
2163
2687
|
process.stderr.write(`Error: ${message}
|
|
2164
2688
|
`);
|
|
@@ -2178,14 +2702,14 @@ function formatComments(result, convertMarkdown) {
|
|
|
2178
2702
|
return lines.join("\n");
|
|
2179
2703
|
}
|
|
2180
2704
|
function createCommentsListCommand() {
|
|
2181
|
-
const command = new
|
|
2705
|
+
const command = new Command13("list");
|
|
2182
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) => {
|
|
2183
2707
|
validateOrgProjectPair(options);
|
|
2184
2708
|
const id = parseWorkItemId(idStr);
|
|
2185
2709
|
let context;
|
|
2186
2710
|
try {
|
|
2187
2711
|
context = resolveContext(options);
|
|
2188
|
-
const credential = await
|
|
2712
|
+
const credential = await requirePat(context.org);
|
|
2189
2713
|
const result = await listWorkItemComments(context, id, credential.pat);
|
|
2190
2714
|
if (options.json) {
|
|
2191
2715
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -2206,7 +2730,7 @@ function createCommentsListCommand() {
|
|
|
2206
2730
|
return command;
|
|
2207
2731
|
}
|
|
2208
2732
|
function createCommentsAddCommand() {
|
|
2209
|
-
const command = new
|
|
2733
|
+
const command = new Command13("add");
|
|
2210
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) => {
|
|
2211
2735
|
validateOrgProjectPair(options);
|
|
2212
2736
|
const id = parseWorkItemId(idStr);
|
|
@@ -2216,7 +2740,7 @@ function createCommentsAddCommand() {
|
|
|
2216
2740
|
let context;
|
|
2217
2741
|
try {
|
|
2218
2742
|
context = resolveContext(options);
|
|
2219
|
-
const credential = await
|
|
2743
|
+
const credential = await requirePat(context.org);
|
|
2220
2744
|
const format = options.markdown === true ? "markdown" : "html";
|
|
2221
2745
|
const result = await addWorkItemComment(context, id, credential.pat, text, format);
|
|
2222
2746
|
if (options.json) {
|
|
@@ -2233,7 +2757,7 @@ function createCommentsAddCommand() {
|
|
|
2233
2757
|
return command;
|
|
2234
2758
|
}
|
|
2235
2759
|
function createCommentsCommand() {
|
|
2236
|
-
const command = new
|
|
2760
|
+
const command = new Command13("comments");
|
|
2237
2761
|
command.description("Manage Azure DevOps work item comments");
|
|
2238
2762
|
command.addCommand(createCommentsListCommand());
|
|
2239
2763
|
command.addCommand(createCommentsAddCommand());
|
|
@@ -2241,12 +2765,12 @@ function createCommentsCommand() {
|
|
|
2241
2765
|
}
|
|
2242
2766
|
|
|
2243
2767
|
// src/commands/download-attachment.ts
|
|
2244
|
-
import { Command as
|
|
2768
|
+
import { Command as Command14 } from "commander";
|
|
2245
2769
|
import { writeFile } from "fs/promises";
|
|
2246
2770
|
import { existsSync as existsSync4 } from "fs";
|
|
2247
2771
|
import { join as join2 } from "path";
|
|
2248
2772
|
function createDownloadAttachmentCommand() {
|
|
2249
|
-
const command = new
|
|
2773
|
+
const command = new Command14("download-attachment");
|
|
2250
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(
|
|
2251
2775
|
async (idStr, filename, options) => {
|
|
2252
2776
|
const id = parseWorkItemId(idStr);
|
|
@@ -2254,7 +2778,7 @@ function createDownloadAttachmentCommand() {
|
|
|
2254
2778
|
let context;
|
|
2255
2779
|
try {
|
|
2256
2780
|
context = resolveContext(options);
|
|
2257
|
-
const credential = await
|
|
2781
|
+
const credential = await requirePat(context.org);
|
|
2258
2782
|
const outputDir = options.output ?? ".";
|
|
2259
2783
|
if (!existsSync4(outputDir)) {
|
|
2260
2784
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
@@ -2288,9 +2812,10 @@ function createDownloadAttachmentCommand() {
|
|
|
2288
2812
|
}
|
|
2289
2813
|
|
|
2290
2814
|
// src/index.ts
|
|
2291
|
-
var program = new
|
|
2815
|
+
var program = new Command15();
|
|
2292
2816
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
2293
2817
|
program.addCommand(createGetItemCommand());
|
|
2818
|
+
program.addCommand(createAuthCommand());
|
|
2294
2819
|
program.addCommand(createClearPatCommand());
|
|
2295
2820
|
program.addCommand(createConfigCommand());
|
|
2296
2821
|
program.addCommand(createSetStateCommand());
|