Mirrowel commited on
Commit
ef8f78e
·
1 Parent(s): 8eb4988

refactor(credentials): 🔨 separate view and manage credential menus

Browse files

This commit refactors the credential management interface by separating view-only and management operations into distinct menus, improving user experience and code organization.

- Add new `view_credentials_menu()` function that provides read-only credential viewing with drill-down capability from summary to provider-specific details
- Create dedicated detail view functions `_view_api_keys_detail()` and `_view_oauth_credentials_detail()` for displaying comprehensive credential information
- Modify `manage_credentials_submenu()` to focus solely on management operations (delete, edit)
- Enhance `clear_screen()` function to accept an optional subtitle parameter, providing contextual headers throughout the application
- Update all credential-related functions to use the new `clear_screen()` signature with descriptive subtitles for better navigation clarity
- Add "View Credentials" as option 4 and "Manage Credentials" as option 5 in main menu
- Improve visual hierarchy in detail views with formatted tables showing masked API keys and OAuth credential metadata
- Streamline export function headers by removing redundant Panel declarations in favor of unified `clear_screen()` calls

Files changed (1) hide show
  1. src/rotator_library/credential_tool.py +200 -71
src/rotator_library/credential_tool.py CHANGED
@@ -541,20 +541,178 @@ async def _edit_oauth_credential_email(provider_name: str):
541
  console.print(f"[bold red]Error editing credential: {e}[/bold red]")
542
 
543
 
544
- async def manage_credentials_submenu():
545
  """
546
- Submenu for viewing and managing all credentials (API keys and OAuth).
547
- Allows deletion of any credential and editing email for OAuth credentials.
548
  """
549
  while True:
550
- clear_screen()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  console.print(
552
  Panel(
553
- "[bold cyan]View / Manage All Credentials[/bold cyan]",
554
- title="--- API Key Proxy ---",
 
555
  )
556
  )
557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  # Display full summary
559
  _display_credentials_summary()
560
 
@@ -603,7 +761,7 @@ async def manage_credentials_submenu():
603
 
604
  async def _delete_api_key_menu():
605
  """Menu for deleting an API key from the .env file."""
606
- clear_screen()
607
  api_keys = _get_api_keys_from_env()
608
 
609
  if not api_keys:
@@ -680,7 +838,7 @@ async def _delete_api_key_menu():
680
 
681
  async def _delete_oauth_credential_menu():
682
  """Menu for deleting an OAuth credential file."""
683
- clear_screen()
684
  oauth_summary = _get_oauth_credentials_summary()
685
 
686
  # Check if there are any credentials
@@ -771,7 +929,7 @@ async def _delete_oauth_credential_menu():
771
 
772
  async def _edit_oauth_credential_menu():
773
  """Menu for editing an OAuth credential's email field."""
774
- clear_screen()
775
  oauth_summary = _get_oauth_credentials_summary()
776
 
777
  # Check if there are any credentials
@@ -826,16 +984,27 @@ async def _edit_oauth_credential_menu():
826
  console.print(f"[bold red]Error: {e}[/bold red]")
827
 
828
 
829
- def clear_screen():
830
  """
831
- Cross-platform terminal clear that works robustly on both
832
- classic Windows conhost and modern terminals (Windows Terminal, Linux, Mac).
 
 
 
 
 
833
 
834
  Uses native OS commands instead of ANSI escape sequences:
835
  - Windows (conhost & Windows Terminal): cls
836
  - Unix-like systems (Linux, Mac): clear
837
  """
838
  os.system("cls" if os.name == "nt" else "clear")
 
 
 
 
 
 
839
 
840
 
841
  def ensure_env_defaults():
@@ -861,8 +1030,7 @@ async def setup_api_key():
861
  """
862
  Interactively sets up a new API key for a provider.
863
  """
864
- clear_screen()
865
- console.print(Panel("[bold cyan]API Key Setup[/bold cyan]", expand=False))
866
 
867
  # Debug toggle: Set to True to see env var names next to each provider
868
  SHOW_ENV_VAR_NAMES = True
@@ -1169,12 +1337,7 @@ async def export_gemini_cli_to_env():
1169
  Export a Gemini CLI credential JSON file to .env format.
1170
  Uses the auth class's build_env_lines() and list_credentials() methods.
1171
  """
1172
- clear_screen()
1173
- console.print(
1174
- Panel(
1175
- "[bold cyan]Export Gemini CLI Credential to .env[/bold cyan]", expand=False
1176
- )
1177
- )
1178
 
1179
  # Get auth instance for this provider
1180
  provider_factory, _ = _ensure_providers_loaded()
@@ -1269,12 +1432,7 @@ async def export_qwen_code_to_env():
1269
  Export a Qwen Code credential JSON file to .env format.
1270
  Uses the auth class's build_env_lines() and list_credentials() methods.
1271
  """
1272
- clear_screen()
1273
- console.print(
1274
- Panel(
1275
- "[bold cyan]Export Qwen Code Credential to .env[/bold cyan]", expand=False
1276
- )
1277
- )
1278
 
1279
  # Get auth instance for this provider
1280
  provider_factory, _ = _ensure_providers_loaded()
@@ -1368,10 +1526,7 @@ async def export_iflow_to_env():
1368
  Export an iFlow credential JSON file to .env format.
1369
  Uses the auth class's build_env_lines() and list_credentials() methods.
1370
  """
1371
- clear_screen()
1372
- console.print(
1373
- Panel("[bold cyan]Export iFlow Credential to .env[/bold cyan]", expand=False)
1374
- )
1375
 
1376
  # Get auth instance for this provider
1377
  provider_factory, _ = _ensure_providers_loaded()
@@ -1465,12 +1620,7 @@ async def export_antigravity_to_env():
1465
  Export an Antigravity credential JSON file to .env format.
1466
  Uses the auth class's build_env_lines() and list_credentials() methods.
1467
  """
1468
- clear_screen()
1469
- console.print(
1470
- Panel(
1471
- "[bold cyan]Export Antigravity Credential to .env[/bold cyan]", expand=False
1472
- )
1473
- )
1474
 
1475
  # Get auth instance for this provider
1476
  provider_factory, _ = _ensure_providers_loaded()
@@ -1565,7 +1715,8 @@ async def export_all_provider_credentials(provider_name: str):
1565
  Export all credentials for a specific provider to individual .env files.
1566
  Uses the auth class's list_credentials() and export_credential_to_env() methods.
1567
  """
1568
- clear_screen()
 
1569
  # Get auth instance for this provider
1570
  provider_factory, _ = _ensure_providers_loaded()
1571
  try:
@@ -1634,7 +1785,8 @@ async def combine_provider_credentials(provider_name: str):
1634
  Combine all credentials for a specific provider into a single .env file.
1635
  Uses the auth class's list_credentials() and build_env_lines() methods.
1636
  """
1637
- clear_screen()
 
1638
  # Get auth instance for this provider
1639
  provider_factory, _ = _ensure_providers_loaded()
1640
  try:
@@ -1719,10 +1871,7 @@ async def combine_all_credentials():
1719
  Combine ALL credentials from ALL providers into a single .env file.
1720
  Uses auth class list_credentials() and build_env_lines() methods.
1721
  """
1722
- clear_screen()
1723
- console.print(
1724
- Panel("[bold cyan]Combine All Provider Credentials[/bold cyan]", expand=False)
1725
- )
1726
 
1727
  # List of providers that support OAuth credentials
1728
  oauth_providers = ["gemini_cli", "qwen_code", "iflow", "antigravity"]
@@ -1820,14 +1969,7 @@ async def export_credentials_submenu():
1820
  Submenu for credential export options.
1821
  """
1822
  while True:
1823
- clear_screen()
1824
- console.print(
1825
- Panel(
1826
- "[bold cyan]Export Credentials to .env[/bold cyan]",
1827
- title="--- API Key Proxy ---",
1828
- expand=False,
1829
- )
1830
- )
1831
 
1832
  console.print(
1833
  Panel(
@@ -1952,22 +2094,11 @@ async def main(clear_on_start=True):
1952
 
1953
  # Only show header if we're clearing (standalone mode)
1954
  if clear_on_start:
1955
- console.print(
1956
- Panel(
1957
- "[bold cyan]Interactive Credential Setup[/bold cyan]",
1958
- title="--- API Key Proxy ---",
1959
- )
1960
- )
1961
 
1962
  while True:
1963
  # Clear screen between menu selections for cleaner UX
1964
  clear_screen()
1965
- console.print(
1966
- Panel(
1967
- "[bold cyan]Interactive Credential Setup[/bold cyan]",
1968
- title="--- API Key Proxy ---",
1969
- )
1970
- )
1971
 
1972
  # Display credentials summary at the top
1973
  _display_credentials_summary()
@@ -1978,7 +2109,8 @@ async def main(clear_on_start=True):
1978
  "1. Add OAuth Credential\n"
1979
  "2. Add API Key\n"
1980
  "3. Export Credentials\n"
1981
- "4. View / Manage All Credentials"
 
1982
  ),
1983
  title="Choose action",
1984
  style="bold blue",
@@ -1989,7 +2121,7 @@ async def main(clear_on_start=True):
1989
  Text.from_markup(
1990
  "[bold]Please select an option or type [red]'q'[/red] to quit[/bold]"
1991
  ),
1992
- choices=["1", "2", "3", "4", "q"],
1993
  show_choices=False,
1994
  )
1995
 
@@ -1998,13 +2130,7 @@ async def main(clear_on_start=True):
1998
 
1999
  if setup_type == "1":
2000
  # Clear and show OAuth providers summary before listing providers
2001
- clear_screen()
2002
- console.print(
2003
- Panel(
2004
- "[bold cyan]Add OAuth Credential[/bold cyan]",
2005
- title="--- API Key Proxy ---",
2006
- )
2007
- )
2008
  _display_oauth_providers_summary()
2009
 
2010
  provider_factory, _ = _ensure_providers_loaded()
@@ -2074,6 +2200,9 @@ async def main(clear_on_start=True):
2074
  await export_credentials_submenu()
2075
 
2076
  elif setup_type == "4":
 
 
 
2077
  await manage_credentials_submenu()
2078
 
2079
 
 
541
  console.print(f"[bold red]Error editing credential: {e}[/bold red]")
542
 
543
 
544
+ async def view_credentials_menu():
545
  """
546
+ Menu for viewing credentials. Shows summary first, then allows drilling
547
+ down to view detailed credentials for a specific provider.
548
  """
549
  while True:
550
+ clear_screen("View Credentials")
551
+
552
+ # Display summary
553
+ _display_credentials_summary()
554
+
555
+ # Build list of all providers with credentials
556
+ api_keys = _get_api_keys_from_env()
557
+ oauth_creds = _get_oauth_credentials_summary()
558
+
559
+ all_providers = []
560
+
561
+ # Add API key providers
562
+ for provider in sorted(api_keys.keys()):
563
+ count = len(api_keys[provider])
564
+ all_providers.append(("api", provider, count))
565
+
566
+ # Add OAuth providers with credentials
567
+ for provider in sorted(oauth_creds.keys()):
568
+ if oauth_creds[provider]:
569
+ count = len(oauth_creds[provider])
570
+ display_name = OAUTH_FRIENDLY_NAMES.get(provider, provider.title())
571
+ all_providers.append(("oauth", provider, count, display_name))
572
+
573
+ if not all_providers:
574
+ console.print("[bold yellow]No credentials configured.[/bold yellow]")
575
+ console.print("\n[dim]Press Enter to return to main menu...[/dim]")
576
+ input()
577
+ break
578
+
579
+ # Display provider selection menu
580
  console.print(
581
  Panel(
582
+ Text.from_markup("[bold]Select a provider to view details:[/bold]"),
583
+ title="View Provider Credentials",
584
+ style="bold blue",
585
  )
586
  )
587
 
588
+ for i, provider_info in enumerate(all_providers, 1):
589
+ if provider_info[0] == "api":
590
+ _, provider, count = provider_info
591
+ console.print(f" {i}. [cyan]API:[/cyan] {provider} ({count} key(s))")
592
+ else:
593
+ _, provider, count, display_name = provider_info
594
+ console.print(
595
+ f" {i}. [cyan]OAuth:[/cyan] {display_name} ({count} credential(s))"
596
+ )
597
+
598
+ choice = Prompt.ask(
599
+ Text.from_markup(
600
+ "\n[bold]Select provider or type [red]'b'[/red] to go back[/bold]"
601
+ ),
602
+ choices=[str(i) for i in range(1, len(all_providers) + 1)] + ["b"],
603
+ show_choices=False,
604
+ )
605
+
606
+ if choice.lower() == "b":
607
+ break
608
+
609
+ try:
610
+ idx = int(choice) - 1
611
+ provider_info = all_providers[idx]
612
+
613
+ if provider_info[0] == "api":
614
+ _, provider, _ = provider_info
615
+ await _view_api_keys_detail(provider)
616
+ else:
617
+ _, provider, _, _ = provider_info
618
+ await _view_oauth_credentials_detail(provider)
619
+
620
+ except (ValueError, IndexError):
621
+ console.print("[bold red]Invalid choice.[/bold red]")
622
+ await asyncio.sleep(1)
623
+
624
+
625
+ async def _view_api_keys_detail(provider_name: str):
626
+ """Display detailed view of API keys for a specific provider."""
627
+ clear_screen(f"View {provider_name} API Keys")
628
+
629
+ api_keys = _get_api_keys_from_env()
630
+ keys = api_keys.get(provider_name, [])
631
+
632
+ if not keys:
633
+ console.print(
634
+ f"[bold yellow]No API keys found for {provider_name}.[/bold yellow]"
635
+ )
636
+ console.print("\n[dim]Press Enter to go back...[/dim]")
637
+ input()
638
+ return
639
+
640
+ # Display detailed table
641
+ table = Table(title=f"{provider_name} API Keys", box=None, padding=(0, 2))
642
+ table.add_column("#", style="dim", width=4)
643
+ table.add_column("Key Name", style="yellow")
644
+ table.add_column("Value (masked)", style="dim")
645
+
646
+ for i, (key_name, key_value) in enumerate(keys, 1):
647
+ masked = f"****{key_value[-4:]}" if len(key_value) > 4 else "****"
648
+ table.add_row(str(i), key_name, masked)
649
+
650
+ console.print(table)
651
+ console.print(f"\n[dim]Total: {len(keys)} key(s)[/dim]")
652
+ console.print("\n[dim]Press Enter to go back...[/dim]")
653
+ input()
654
+
655
+
656
+ async def _view_oauth_credentials_detail(provider_name: str):
657
+ """Display detailed view of OAuth credentials for a specific provider."""
658
+ display_name = OAUTH_FRIENDLY_NAMES.get(provider_name, provider_name.title())
659
+ clear_screen(f"View {display_name} Credentials")
660
+
661
+ provider_factory, _ = _ensure_providers_loaded()
662
+
663
+ try:
664
+ auth_class = provider_factory.get_provider_auth_class(provider_name)
665
+ auth_instance = auth_class()
666
+ credentials = auth_instance.list_credentials(_get_oauth_base_dir())
667
+ except Exception:
668
+ credentials = []
669
+
670
+ if not credentials:
671
+ console.print(
672
+ f"[bold yellow]No credentials found for {display_name}.[/bold yellow]"
673
+ )
674
+ console.print("\n[dim]Press Enter to go back...[/dim]")
675
+ input()
676
+ return
677
+
678
+ # Display detailed table
679
+ table = Table(title=f"{display_name} Credentials", box=None, padding=(0, 2))
680
+ table.add_column("#", style="dim", width=4)
681
+ table.add_column("File", style="yellow")
682
+ table.add_column("Email/Identifier", style="cyan")
683
+
684
+ # Add tier/project columns for Google OAuth providers
685
+ if provider_name in ["gemini_cli", "antigravity"]:
686
+ table.add_column("Tier", style="green")
687
+ table.add_column("Project", style="dim")
688
+
689
+ for i, cred in enumerate(credentials, 1):
690
+ file_name = Path(cred["file_path"]).name
691
+ email = cred.get("email", "unknown")
692
+
693
+ if provider_name in ["gemini_cli", "antigravity"]:
694
+ tier = _normalize_tier_name(cred.get("tier")) if cred.get("tier") else "-"
695
+ project = cred.get("project_id", "-")
696
+ if project and len(project) > 25:
697
+ project = project[:22] + "..."
698
+ table.add_row(str(i), file_name, email, tier, project or "-")
699
+ else:
700
+ table.add_row(str(i), file_name, email)
701
+
702
+ console.print(table)
703
+ console.print(f"\n[dim]Total: {len(credentials)} credential(s)[/dim]")
704
+ console.print("\n[dim]Press Enter to go back...[/dim]")
705
+ input()
706
+
707
+
708
+ async def manage_credentials_submenu():
709
+ """
710
+ Submenu for viewing and managing all credentials (API keys and OAuth).
711
+ Allows deletion of any credential and editing email for OAuth credentials.
712
+ """
713
+ while True:
714
+ clear_screen("Manage Credentials")
715
+
716
  # Display full summary
717
  _display_credentials_summary()
718
 
 
761
 
762
  async def _delete_api_key_menu():
763
  """Menu for deleting an API key from the .env file."""
764
+ clear_screen("Delete API Key")
765
  api_keys = _get_api_keys_from_env()
766
 
767
  if not api_keys:
 
838
 
839
  async def _delete_oauth_credential_menu():
840
  """Menu for deleting an OAuth credential file."""
841
+ clear_screen("Delete OAuth Credential")
842
  oauth_summary = _get_oauth_credentials_summary()
843
 
844
  # Check if there are any credentials
 
929
 
930
  async def _edit_oauth_credential_menu():
931
  """Menu for editing an OAuth credential's email field."""
932
+ clear_screen("Edit OAuth Credential")
933
  oauth_summary = _get_oauth_credentials_summary()
934
 
935
  # Check if there are any credentials
 
984
  console.print(f"[bold red]Error: {e}[/bold red]")
985
 
986
 
987
+ def clear_screen(subtitle: str = "Interactive Credential Setup"):
988
  """
989
+ Cross-platform terminal clear with header display.
990
+
991
+ Clears the terminal and displays the application header with an optional subtitle.
992
+
993
+ Args:
994
+ subtitle: The subtitle text to display in the header panel.
995
+ Defaults to "Interactive Credential Setup".
996
 
997
  Uses native OS commands instead of ANSI escape sequences:
998
  - Windows (conhost & Windows Terminal): cls
999
  - Unix-like systems (Linux, Mac): clear
1000
  """
1001
  os.system("cls" if os.name == "nt" else "clear")
1002
+ console.print(
1003
+ Panel(
1004
+ f"[bold cyan]{subtitle}[/bold cyan]",
1005
+ title="--- API Key Proxy ---",
1006
+ )
1007
+ )
1008
 
1009
 
1010
  def ensure_env_defaults():
 
1030
  """
1031
  Interactively sets up a new API key for a provider.
1032
  """
1033
+ clear_screen("Add API Key")
 
1034
 
1035
  # Debug toggle: Set to True to see env var names next to each provider
1036
  SHOW_ENV_VAR_NAMES = True
 
1337
  Export a Gemini CLI credential JSON file to .env format.
1338
  Uses the auth class's build_env_lines() and list_credentials() methods.
1339
  """
1340
+ clear_screen("Export Gemini CLI Credential")
 
 
 
 
 
1341
 
1342
  # Get auth instance for this provider
1343
  provider_factory, _ = _ensure_providers_loaded()
 
1432
  Export a Qwen Code credential JSON file to .env format.
1433
  Uses the auth class's build_env_lines() and list_credentials() methods.
1434
  """
1435
+ clear_screen("Export Qwen Code Credential")
 
 
 
 
 
1436
 
1437
  # Get auth instance for this provider
1438
  provider_factory, _ = _ensure_providers_loaded()
 
1526
  Export an iFlow credential JSON file to .env format.
1527
  Uses the auth class's build_env_lines() and list_credentials() methods.
1528
  """
1529
+ clear_screen("Export iFlow Credential")
 
 
 
1530
 
1531
  # Get auth instance for this provider
1532
  provider_factory, _ = _ensure_providers_loaded()
 
1620
  Export an Antigravity credential JSON file to .env format.
1621
  Uses the auth class's build_env_lines() and list_credentials() methods.
1622
  """
1623
+ clear_screen("Export Antigravity Credential")
 
 
 
 
 
1624
 
1625
  # Get auth instance for this provider
1626
  provider_factory, _ = _ensure_providers_loaded()
 
1715
  Export all credentials for a specific provider to individual .env files.
1716
  Uses the auth class's list_credentials() and export_credential_to_env() methods.
1717
  """
1718
+ display_name = provider_name.replace("_", " ").title()
1719
+ clear_screen(f"Export All {display_name} Credentials")
1720
  # Get auth instance for this provider
1721
  provider_factory, _ = _ensure_providers_loaded()
1722
  try:
 
1785
  Combine all credentials for a specific provider into a single .env file.
1786
  Uses the auth class's list_credentials() and build_env_lines() methods.
1787
  """
1788
+ display_name = provider_name.replace("_", " ").title()
1789
+ clear_screen(f"Combine {display_name} Credentials")
1790
  # Get auth instance for this provider
1791
  provider_factory, _ = _ensure_providers_loaded()
1792
  try:
 
1871
  Combine ALL credentials from ALL providers into a single .env file.
1872
  Uses auth class list_credentials() and build_env_lines() methods.
1873
  """
1874
+ clear_screen("Combine All Credentials")
 
 
 
1875
 
1876
  # List of providers that support OAuth credentials
1877
  oauth_providers = ["gemini_cli", "qwen_code", "iflow", "antigravity"]
 
1969
  Submenu for credential export options.
1970
  """
1971
  while True:
1972
+ clear_screen("Export Credentials")
 
 
 
 
 
 
 
1973
 
1974
  console.print(
1975
  Panel(
 
2094
 
2095
  # Only show header if we're clearing (standalone mode)
2096
  if clear_on_start:
2097
+ clear_screen()
 
 
 
 
 
2098
 
2099
  while True:
2100
  # Clear screen between menu selections for cleaner UX
2101
  clear_screen()
 
 
 
 
 
 
2102
 
2103
  # Display credentials summary at the top
2104
  _display_credentials_summary()
 
2109
  "1. Add OAuth Credential\n"
2110
  "2. Add API Key\n"
2111
  "3. Export Credentials\n"
2112
+ "4. View Credentials\n"
2113
+ "5. Manage Credentials"
2114
  ),
2115
  title="Choose action",
2116
  style="bold blue",
 
2121
  Text.from_markup(
2122
  "[bold]Please select an option or type [red]'q'[/red] to quit[/bold]"
2123
  ),
2124
+ choices=["1", "2", "3", "4", "5", "q"],
2125
  show_choices=False,
2126
  )
2127
 
 
2130
 
2131
  if setup_type == "1":
2132
  # Clear and show OAuth providers summary before listing providers
2133
+ clear_screen("Add OAuth Credential")
 
 
 
 
 
 
2134
  _display_oauth_providers_summary()
2135
 
2136
  provider_factory, _ = _ensure_providers_loaded()
 
2200
  await export_credentials_submenu()
2201
 
2202
  elif setup_type == "4":
2203
+ await view_credentials_menu()
2204
+
2205
+ elif setup_type == "5":
2206
  await manage_credentials_submenu()
2207
 
2208