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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +706 -183
  3. 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 Command14 } from "commander";
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
- var SERVICE = "azdo-cli";
371
- var ACCOUNT = "pat";
372
- async function getPat() {
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
- const entry = new Entry(SERVICE, ACCOUNT);
375
- return entry.getPassword();
397
+ fs.chmodSync(dir, 448);
376
398
  } catch {
377
- return null;
378
399
  }
379
400
  }
380
- async function storePat(pat) {
401
+ function ensureFileWithPerms(file) {
402
+ if (!fs.existsSync(file)) {
403
+ fs.writeFileSync(file, "", { mode: 384 });
404
+ return;
405
+ }
381
406
  try {
382
- const entry = new Entry(SERVICE, ACCOUNT);
383
- entry.setPassword(pat);
384
- return true;
407
+ fs.chmodSync(file, 384);
385
408
  } catch {
386
- return false;
387
409
  }
388
410
  }
389
- async function deletePat() {
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
- const entry = new Entry(SERVICE, ACCOUNT);
392
- entry.deletePassword();
393
- return true;
497
+ return JSON.parse(raw);
394
498
  } catch {
395
- return false;
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(promptFn = promptForPat) {
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
- const promptedPat = await promptFn();
492
- if (promptedPat !== null) {
493
- const normalizedPat = normalizePat(promptedPat);
494
- if (normalizedPat !== null) {
495
- const saved = await storePat(normalizedPat);
496
- if (!saved) {
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
- "Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
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/config-store.ts
578
- import fs from "fs";
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
- raw = fs.readFileSync(configPath, "utf-8");
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
- process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
630
- `);
631
- return {};
870
+ return null;
632
871
  }
633
872
  }
634
- function saveConfig(config) {
635
- const configPath = getConfigPath();
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 validateKey(key) {
641
- if (!VALID_KEYS.includes(key)) {
642
- throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
876
+ function resolveOrg(options) {
877
+ if (options.org && options.org.length > 0) {
878
+ return { org: options.org, source: "flag" };
643
879
  }
644
- }
645
- function getConfigValue(key) {
646
- validateKey(key);
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
- saveConfig(config);
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 unsetConfigValue(key) {
668
- validateKey(key);
669
- const config = loadConfig();
670
- delete config[key];
671
- saveConfig(config);
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
- if (options.org && options.project) {
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 org = config.org || gitContext?.org;
689
- const project = config.project || gitContext?.project;
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 resolvePat();
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 the stored Azure DevOps PAT from the credential store").action(async () => {
987
- const deleted = await deletePat();
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("PAT removed from credential store.\n");
1220
+ process.stdout.write(`PAT removed for org ${resolved.org}.
1221
+ `);
990
1222
  } else {
991
- process.stdout.write("No stored PAT found.\n");
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/config.ts
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 Command3("config");
1580
+ const config = new Command4("config");
1059
1581
  config.description("Manage CLI settings");
1060
- const set = new Command3("set");
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 Command3("get");
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 Command3("list");
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 Command3("unset");
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 Command3("wizard");
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 Command4 } from "commander";
1683
+ import { Command as Command5 } from "commander";
1162
1684
  function createSetStateCommand() {
1163
- const command = new Command4("set-state");
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 resolvePat();
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 Command5 } from "commander";
1721
+ import { Command as Command6 } from "commander";
1200
1722
  function createAssignCommand() {
1201
- const command = new Command5("assign");
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 resolvePat();
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 Command6 } from "commander";
1773
+ import { Command as Command7 } from "commander";
1252
1774
  function createSetFieldCommand() {
1253
- const command = new Command6("set-field");
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 resolvePat();
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 Command7 } from "commander";
1811
+ import { Command as Command8 } from "commander";
1290
1812
  function createGetMdFieldCommand() {
1291
- const command = new Command7("get-md-field");
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 resolvePat();
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 Command8 } from "commander";
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 Command8("set-md-field");
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 resolvePat();
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 Command9 } from "commander";
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 Command9("upsert");
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 resolvePat();
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 Command10 } from "commander";
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 Command10("list-fields");
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 resolvePat();
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 Command11 } from "commander";
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 resolvePat();
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 Command11("status");
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 Command11("open");
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 Command11("comments");
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 Command11("pr");
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 Command12 } from "commander";
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 Command12("list");
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 resolvePat();
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 Command12("add");
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 resolvePat();
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 Command12("comments");
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 Command13 } from "commander";
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 Command13("download-attachment");
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 resolvePat();
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 Command14();
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.10.0-develop.213",
3
+ "version": "0.10.0-develop.233",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {