-
Notifications
You must be signed in to change notification settings - Fork 3
Profiles #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Profiles #14
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
123ad46
feat: Initial implementation of profiles with Bob
gabe-l-hart b8044eb
feat: Refactor Bob's version for cleaner centralization of base URL
gabe-l-hart 44b188b
fix: Put whole id in profile table
gabe-l-hart ea282c5
fix: (unrelated) fix isActive -> enabled that got missed in prompts
gabe-l-hart 759ad9a
fix: Only open the store file for reading if it exists
gabe-l-hart a915956
test: Update tests to mock get_base_url correctly
gabe-l-hart 1c63595
fix: Refactor the structure of the profiles command to match the others
gabe-l-hart 3cff323
test: Add tests for profile utils and new common helper
gabe-l-hart 0cb9ca8
test: Add tests for profiles commands
gabe-l-hart 912c17e
feat: Add validation logic to profile structures
gabe-l-hart 7f256ed
test: Full test coverage for profiles commands
gabe-l-hart cb46a68
fix: Remove unreachable validation condition
gabe-l-hart 8fc4e94
test: Unit tests for validation errors
gabe-l-hart 96a41cc
feat: Use a suffix for per-profile tokens
gabe-l-hart 938466a
feat: Report active profile in whoami
gabe-l-hart 3b55c3a
feat: Implement auto-login matching Desktop app stored credentials
gabe-l-hart a6a9920
chore: Explicitly add cryptography dep
gabe-l-hart e1cc95e
test: Add unit tests for credential_store.py
gabe-l-hart 5515d53
test: Add tests to cover auto-login in common
gabe-l-hart 3347a06
feat: Add cforge profiles create
gabe-l-hart 00cf980
feat: Add logic to manage a virtual default profile
gabe-l-hart 7749adb
test(fix): Fix the mock_everywhere test util to also mock in conftest
gabe-l-hart 027e286
fix: Remove unreachable code paths that handled no active profile
gabe-l-hart 0062ba7
:
gabe-l-hart f7eaca3
feat: Don't show virtual default if desktop default is present
gabe-l-hart 8bf63e0
fix: Remove profiles_current
gabe-l-hart 7a688e5
fix: Remove unnecessary if guard for active profile in whoami
gabe-l-hart 10cbf7f
fix: Fix get_active_profile to always return a profile
gabe-l-hart 08b55d9
feat: Add the ability to create a profile from a data file
gabe-l-hart 9d8ead4
fix: No need to handle bad state profile store
gabe-l-hart 51fc54d
test: Cover the case of a bad data file for profiles_create
gabe-l-hart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,259 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """Location: ./cforge/commands/profiles.py | ||
| Copyright 2025 | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| Authors: Gabe Goodhart | ||
|
|
||
| CLI commands for profile management | ||
| """ | ||
|
|
||
| # Standard | ||
| from datetime import datetime | ||
| from typing import Optional | ||
| import secrets | ||
| import string | ||
|
|
||
| # Third-Party | ||
| import typer | ||
|
|
||
| # First-Party | ||
| from cforge.common import get_console, print_table, print_json, prompt_for_schema | ||
| from cforge.config import get_settings | ||
| from cforge.profile_utils import ( | ||
| AuthProfile, | ||
| ProfileStore, | ||
| get_all_profiles, | ||
| get_profile, | ||
| get_active_profile, | ||
| set_active_profile, | ||
| load_profile_store, | ||
| save_profile_store, | ||
| ) | ||
|
|
||
|
|
||
| def profiles_list() -> None: | ||
| """List all available profiles. | ||
|
|
||
| Displays all profiles configured in the Desktop app, showing their name, | ||
| email, API URL, and active status. | ||
| """ | ||
| console = get_console() | ||
|
|
||
| try: | ||
| profiles = get_all_profiles() | ||
|
|
||
| # Prepare data for table | ||
| profile_data = [] | ||
| for profile in profiles: | ||
| profile_data.append( | ||
| { | ||
| "id": profile.id, | ||
| "name": profile.name, | ||
| "email": profile.email, | ||
| "api_url": profile.api_url, | ||
| "active": "✓" if profile.is_active else "", | ||
| "environment": profile.metadata.environment if profile.metadata else "", | ||
| } | ||
| ) | ||
|
|
||
| print_table( | ||
| profile_data, | ||
| "Available Profiles", | ||
| ["id", "name", "email", "api_url", "environment", "active"], | ||
| ) | ||
|
|
||
| # Show which profile is currently active | ||
| active = get_active_profile() | ||
| console.print(f"\n[green]Currently using profile:[/green] [cyan]{active.name}[/cyan] ({active.email})") | ||
| console.print(f"[dim]Connected to: {active.api_url}[/dim]") | ||
|
|
||
| except Exception as e: | ||
| console.print(f"[red]Error listing profiles: {str(e)}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
|
|
||
| def profiles_get( | ||
| profile_id: Optional[str] = typer.Argument( | ||
| None, | ||
| help="Profile ID to retrieve. If not provided, shows the active profile.", | ||
| ), | ||
| json_output: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Output in JSON format", | ||
| ), | ||
| ) -> None: | ||
| """Get details of a specific profile or the active profile. | ||
|
|
||
| If no profile ID is provided, displays information about the currently | ||
| active profile. | ||
| """ | ||
| console = get_console() | ||
|
|
||
| try: | ||
| if profile_id: | ||
| profile = get_profile(profile_id) | ||
| if not profile: | ||
| console.print(f"[red]Profile not found: {profile_id}[/red]") | ||
| raise typer.Exit(1) | ||
| else: | ||
| profile = get_active_profile() | ||
|
|
||
| if json_output: | ||
| # Output as JSON | ||
| print_json(profile.model_dump(by_alias=True), title="Profile Details") | ||
| else: | ||
| # Pretty print profile details | ||
| console.print(f"\n[bold cyan]Profile: {profile.name}[/bold cyan]") | ||
| console.print(f"[dim]ID:[/dim] {profile.id}") | ||
| console.print(f"[dim]Email:[/dim] {profile.email}") | ||
| console.print(f"[dim]API URL:[/dim] {profile.api_url}") | ||
| console.print(f"[dim]Active:[/dim] {'[green]Yes[/green]' if profile.is_active else '[yellow]No[/yellow]'}") | ||
| console.print(f"[dim]Created:[/dim] {profile.created_at}") | ||
| if profile.last_used: | ||
| console.print(f"[dim]Last Used:[/dim] {profile.last_used}") | ||
|
|
||
| if profile.metadata: | ||
| console.print("\n[bold]Metadata:[/bold]") | ||
| if profile.metadata.description: | ||
| console.print(f" [dim]Description:[/dim] {profile.metadata.description}") | ||
| if profile.metadata.environment: | ||
| console.print(f" [dim]Environment:[/dim] {profile.metadata.environment}") | ||
| if profile.metadata.icon: | ||
| console.print(f" [dim]Icon:[/dim] {profile.metadata.icon}") | ||
|
|
||
| except Exception as e: | ||
| console.print(f"[red]Error retrieving profile: {str(e)}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
|
|
||
| def profiles_switch( | ||
| profile_id: str = typer.Argument( | ||
| ..., | ||
| help="Profile ID to switch to. Use 'cforge profiles list' to see available profiles.", | ||
| ), | ||
| ) -> None: | ||
| """Switch to a different profile. | ||
|
|
||
| Sets the specified profile as the active profile. All subsequent CLI | ||
| commands will use this profile's API URL for connections. | ||
|
|
||
| Note: This only changes which profile the CLI uses. To fully authenticate | ||
| and manage profiles, use the Context Forge Desktop app. | ||
| """ | ||
| console = get_console() | ||
|
|
||
| try: | ||
| # Check if profile exists | ||
| profile = get_profile(profile_id) | ||
| if not profile: | ||
| console.print(f"[red]Profile not found: {profile_id}[/red]") | ||
| console.print("[dim]Use 'cforge profiles list' to see available profiles.[/dim]") | ||
| raise typer.Exit(1) | ||
|
|
||
| # Switch to the profile | ||
| success = set_active_profile(profile_id) | ||
| if not success: | ||
| console.print(f"[red]Failed to switch to profile: {profile_id}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
| console.print(f"[green]✓ Switched to profile:[/green] [cyan]{profile.name}[/cyan]") | ||
| console.print(f"[dim]Email:[/dim] {profile.email}") | ||
| console.print(f"[dim]API URL:[/dim] {profile.api_url}") | ||
|
|
||
| # Clear the settings cache so the new profile takes effect | ||
| get_settings.cache_clear() | ||
|
|
||
| console.print("\n[yellow]Note:[/yellow] Profile switched successfully. " "The CLI will now connect to the selected profile's API URL.") | ||
|
|
||
| except Exception as e: | ||
| console.print(f"[red]Error switching profile: {str(e)}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
|
|
||
| def profiles_current() -> None: | ||
| """Show the currently active profile. | ||
|
|
||
| Displays information about which profile is currently being used by the CLI. | ||
| """ | ||
| console = get_console() | ||
|
|
||
| try: | ||
| profile = get_active_profile() | ||
|
|
||
| console.print(f"\n[bold green]Current Profile:[/bold green] [cyan]{profile.name}[/cyan]") | ||
| console.print(f"[dim]Email:[/dim] {profile.email}") | ||
| console.print(f"[dim]API URL:[/dim] {profile.api_url}") | ||
| if profile.metadata and profile.metadata.environment: | ||
| console.print(f"[dim]Environment:[/dim] {profile.metadata.environment}") | ||
|
|
||
| except Exception as e: | ||
| console.print(f"[red]Error retrieving current profile: {str(e)}[/red]") | ||
| raise typer.Exit(1) | ||
|
|
||
|
|
||
| def profiles_create() -> None: | ||
|
gabe-l-hart marked this conversation as resolved.
Outdated
|
||
| """Create a new profile interactively. | ||
|
|
||
| Walks the user through creating a new profile by prompting for all required | ||
| fields. The new profile will be created in an inactive state. After creation, | ||
| you will be asked if you want to enable the new profile. | ||
| """ | ||
| console = get_console() | ||
|
|
||
| try: | ||
| console.print("\n[bold cyan]Create New Profile[/bold cyan]") | ||
| console.print("[dim]You will be prompted for profile information.[/dim]\n") | ||
|
|
||
| # Generate a 16-character random ID (matching desktop app format) | ||
| alphabet = string.ascii_letters + string.digits | ||
| profile_id = "".join(secrets.choice(alphabet) for _ in range(16)) | ||
| created_at = datetime.now() | ||
|
|
||
| # Pre-fill fields that should not be prompted | ||
| prefilled = { | ||
| "id": profile_id, | ||
| "is_active": False, | ||
| "created_at": created_at, | ||
| "last_used": None, | ||
| } | ||
|
|
||
| # Prompt for profile data using the schema | ||
| profile_data = prompt_for_schema(AuthProfile, prefilled=prefilled) | ||
|
|
||
| # Create the AuthProfile instance | ||
| new_profile = AuthProfile.model_validate(profile_data) | ||
|
|
||
| # Load or create the profile store | ||
| store = load_profile_store() | ||
| if not store: | ||
| store = ProfileStore(profiles={}, active_profile_id=None) | ||
|
|
||
| # Add the new profile to the store | ||
| store.profiles[new_profile.id] = new_profile | ||
|
|
||
| # Save the profile store | ||
| save_profile_store(store) | ||
|
|
||
| console.print("\n[green]✓ Profile created successfully![/green]") | ||
| console.print(f"[dim]Profile ID:[/dim] {new_profile.id}") | ||
| console.print(f"[dim]Name:[/dim] {new_profile.name}") | ||
| console.print(f"[dim]Email:[/dim] {new_profile.email}") | ||
| console.print(f"[dim]API URL:[/dim] {new_profile.api_url}") | ||
|
|
||
| # Ask if the user wants to enable the new profile | ||
| console.print("\n[yellow]Enable this profile now?[/yellow]", end=" ") | ||
| if typer.confirm("", default=False): | ||
| success = set_active_profile(new_profile.id) | ||
| if success: | ||
| console.print(f"[green]✓ Profile enabled:[/green] [cyan]{new_profile.name}[/cyan]") | ||
| # Clear the settings cache so the new profile takes effect | ||
| get_settings.cache_clear() | ||
| else: | ||
| console.print(f"[red]Failed to enable profile: {new_profile.id}[/red]") | ||
| else: | ||
| console.print("[dim]Profile created but not enabled. Use 'cforge profiles switch' to enable it later.[/dim]") | ||
|
|
||
| except Exception as e: | ||
| console.print(f"[red]Error creating profile: {str(e)}[/red]") | ||
| raise typer.Exit(1) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.