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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +708 -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;
@@ -417,7 +693,8 @@ async function promptForPat() {
417
693
  return new Promise((resolve2) => {
418
694
  const rl = createInterface({
419
695
  input: process.stdin,
420
- output: process.stderr
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(promptFn = promptForPat) {
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
- const promptedPat = await promptFn();
490
- if (promptedPat !== null) {
491
- const normalizedPat = normalizePat(promptedPat);
492
- if (normalizedPat !== null) {
493
- const saved = await storePat(normalizedPat);
494
- if (!saved) {
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
- "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}\`.`
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/config-store.ts
576
- import fs from "fs";
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 JSON.parse(raw);
868
+ return detectAzdoContext().org ?? null;
626
869
  } catch {
627
- process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
628
- `);
629
- return {};
870
+ return null;
630
871
  }
631
872
  }
632
- function saveConfig(config) {
633
- const configPath = getConfigPath();
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 validateKey(key) {
639
- if (!VALID_KEYS.includes(key)) {
640
- 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" };
641
879
  }
642
- }
643
- function getConfigValue(key) {
644
- validateKey(key);
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
- 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;
664
889
  }
665
- function unsetConfigValue(key) {
666
- validateKey(key);
667
- const config = loadConfig();
668
- delete config[key];
669
- 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");
670
897
  }
671
898
 
672
899
  // src/services/context.ts
673
900
  function resolveContext(options) {
674
- if (options.org && options.project) {
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 org = config.org || gitContext?.org;
687
- 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;
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 resolvePat();
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 the stored Azure DevOps PAT from the credential store").action(async () => {
985
- 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);
986
1219
  if (deleted) {
987
- process.stdout.write("PAT removed from credential store.\n");
1220
+ process.stdout.write(`PAT removed for org ${resolved.org}.
1221
+ `);
988
1222
  } else {
989
- process.stdout.write("No stored PAT found.\n");
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/config.ts
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 Command3("config");
1580
+ const config = new Command4("config");
1057
1581
  config.description("Manage CLI settings");
1058
- const set = new Command3("set");
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 Command3("get");
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 Command3("list");
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 Command3("unset");
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 Command3("wizard");
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 Command4 } from "commander";
1683
+ import { Command as Command5 } from "commander";
1160
1684
  function createSetStateCommand() {
1161
- const command = new Command4("set-state");
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 resolvePat();
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 Command5 } from "commander";
1721
+ import { Command as Command6 } from "commander";
1198
1722
  function createAssignCommand() {
1199
- const command = new Command5("assign");
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 resolvePat();
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 Command6 } from "commander";
1773
+ import { Command as Command7 } from "commander";
1250
1774
  function createSetFieldCommand() {
1251
- const command = new Command6("set-field");
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 resolvePat();
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 Command7 } from "commander";
1811
+ import { Command as Command8 } from "commander";
1288
1812
  function createGetMdFieldCommand() {
1289
- const command = new Command7("get-md-field");
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 resolvePat();
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 Command8 } from "commander";
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 Command8("set-md-field");
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 resolvePat();
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 Command9 } from "commander";
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 Command9("upsert");
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 resolvePat();
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 Command10 } from "commander";
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 Command10("list-fields");
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 resolvePat();
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 Command11 } from "commander";
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 resolvePat();
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 Command11("status");
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 Command11("open");
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 Command11("comments");
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 Command11("pr");
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 Command12 } from "commander";
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 Command12("list");
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 resolvePat();
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 Command12("add");
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 resolvePat();
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 Command12("comments");
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 Command13 } from "commander";
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 Command13("download-attachment");
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 resolvePat();
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 Command14();
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.10.0-develop.210",
3
+ "version": "0.10.0-develop.232",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {