Skip to content

Commit c309171

Browse files
authored
Merge pull request #14 from contextforge-org/Profiles-13
Profiles
2 parents 79c998f + 51fc54d commit c309171

19 files changed

Lines changed: 3223 additions & 75 deletions

cforge/commands/resources/prompts.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ def prompts_list(
5050
print_table(
5151
prompts,
5252
"Prompts",
53-
["id", "name", "description", "arguments", "isActive"],
54-
{"isActive": "enabled"},
53+
["id", "name", "description", "arguments", "enabled"],
5554
)
5655
else:
5756
console.print("[yellow]No prompts found[/yellow]")

cforge/commands/settings/export.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import typer
1818

1919
# First-Party
20-
from cforge.common import get_console, get_settings, make_authenticated_request
20+
from cforge.common import get_base_url, get_console, make_authenticated_request
2121

2222

2323
def export(
@@ -33,7 +33,7 @@ def export(
3333
console = get_console()
3434

3535
try:
36-
console.print(f"[cyan]Exporting configuration from gateway at http://{get_settings().host}:{get_settings().port}[/cyan]")
36+
console.print(f"[cyan]Exporting configuration from gateway at {get_base_url()}[/cyan]")
3737

3838
# Build API parameters
3939
params: Dict[str, Any] = {}

cforge/commands/settings/login.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import typer
1313

1414
# First-Party
15-
from cforge.common import get_console, get_settings, get_token_file, save_token
15+
from cforge.common import get_base_url, get_console, get_token_file, save_token
1616

1717

1818
def login(
@@ -28,7 +28,7 @@ def login(
2828

2929
try:
3030
# Make login request
31-
gateway_url = f"http://{get_settings().host}:{get_settings().port}"
31+
gateway_url = get_base_url()
3232
full_url = f"{gateway_url}/auth/login"
3333

3434
response = requests.post(full_url, json={"email": email, "password": password})
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# -*- coding: utf-8 -*-
2+
"""Location: ./cforge/commands/profiles.py
3+
Copyright 2025
4+
SPDX-License-Identifier: Apache-2.0
5+
Authors: Gabe Goodhart
6+
7+
CLI commands for profile management
8+
"""
9+
10+
# Standard
11+
from datetime import datetime
12+
from pathlib import Path
13+
from typing import Optional
14+
import json
15+
import secrets
16+
import string
17+
18+
# Third-Party
19+
import typer
20+
21+
# First-Party
22+
from cforge.common import get_console, print_table, print_json, prompt_for_schema
23+
from cforge.config import get_settings
24+
from cforge.profile_utils import (
25+
AuthProfile,
26+
ProfileStore,
27+
get_all_profiles,
28+
get_profile,
29+
get_active_profile,
30+
set_active_profile,
31+
load_profile_store,
32+
save_profile_store,
33+
)
34+
35+
36+
def profiles_list() -> None:
37+
"""List all available profiles.
38+
39+
Displays all profiles configured in the Desktop app, showing their name,
40+
email, API URL, and active status.
41+
"""
42+
console = get_console()
43+
44+
try:
45+
profiles = get_all_profiles()
46+
47+
# Prepare data for table
48+
profile_data = []
49+
for profile in profiles:
50+
profile_data.append(
51+
{
52+
"id": profile.id,
53+
"name": profile.name,
54+
"email": profile.email,
55+
"api_url": profile.api_url,
56+
"active": "✓" if profile.is_active else "",
57+
"environment": profile.metadata.environment if profile.metadata else "",
58+
}
59+
)
60+
61+
print_table(
62+
profile_data,
63+
"Available Profiles",
64+
["id", "name", "email", "api_url", "environment", "active"],
65+
)
66+
67+
# Show which profile is currently active
68+
active = get_active_profile()
69+
console.print(f"\n[green]Currently using profile:[/green] [cyan]{active.name}[/cyan] ({active.email})")
70+
console.print(f"[dim]Connected to: {active.api_url}[/dim]")
71+
72+
except Exception as e:
73+
console.print(f"[red]Error listing profiles: {str(e)}[/red]")
74+
raise typer.Exit(1)
75+
76+
77+
def profiles_get(
78+
profile_id: Optional[str] = typer.Argument(
79+
None,
80+
help="Profile ID to retrieve. If not provided, shows the active profile.",
81+
),
82+
json_output: bool = typer.Option(
83+
False,
84+
"--json",
85+
help="Output in JSON format",
86+
),
87+
) -> None:
88+
"""Get details of a specific profile or the active profile.
89+
90+
If no profile ID is provided, displays information about the currently
91+
active profile.
92+
"""
93+
console = get_console()
94+
95+
try:
96+
if profile_id:
97+
profile = get_profile(profile_id)
98+
if not profile:
99+
console.print(f"[red]Profile not found: {profile_id}[/red]")
100+
raise typer.Exit(1)
101+
else:
102+
profile = get_active_profile()
103+
104+
if json_output:
105+
# Output as JSON
106+
print_json(profile.model_dump(by_alias=True), title="Profile Details")
107+
else:
108+
# Pretty print profile details
109+
console.print(f"\n[bold cyan]Profile: {profile.name}[/bold cyan]")
110+
console.print(f"[dim]ID:[/dim] {profile.id}")
111+
console.print(f"[dim]Email:[/dim] {profile.email}")
112+
console.print(f"[dim]API URL:[/dim] {profile.api_url}")
113+
console.print(f"[dim]Active:[/dim] {'[green]Yes[/green]' if profile.is_active else '[yellow]No[/yellow]'}")
114+
console.print(f"[dim]Created:[/dim] {profile.created_at}")
115+
if profile.last_used:
116+
console.print(f"[dim]Last Used:[/dim] {profile.last_used}")
117+
118+
if profile.metadata:
119+
console.print("\n[bold]Metadata:[/bold]")
120+
if profile.metadata.description:
121+
console.print(f" [dim]Description:[/dim] {profile.metadata.description}")
122+
if profile.metadata.environment:
123+
console.print(f" [dim]Environment:[/dim] {profile.metadata.environment}")
124+
if profile.metadata.icon:
125+
console.print(f" [dim]Icon:[/dim] {profile.metadata.icon}")
126+
127+
except Exception as e:
128+
console.print(f"[red]Error retrieving profile: {str(e)}[/red]")
129+
raise typer.Exit(1)
130+
131+
132+
def profiles_switch(
133+
profile_id: str = typer.Argument(
134+
...,
135+
help="Profile ID to switch to. Use 'cforge profiles list' to see available profiles.",
136+
),
137+
) -> None:
138+
"""Switch to a different profile.
139+
140+
Sets the specified profile as the active profile. All subsequent CLI
141+
commands will use this profile's API URL for connections.
142+
143+
Note: This only changes which profile the CLI uses. To fully authenticate
144+
and manage profiles, use the Context Forge Desktop app.
145+
"""
146+
console = get_console()
147+
148+
try:
149+
# Check if profile exists
150+
profile = get_profile(profile_id)
151+
if not profile:
152+
console.print(f"[red]Profile not found: {profile_id}[/red]")
153+
console.print("[dim]Use 'cforge profiles list' to see available profiles.[/dim]")
154+
raise typer.Exit(1)
155+
156+
# Switch to the profile
157+
success = set_active_profile(profile_id)
158+
if not success:
159+
console.print(f"[red]Failed to switch to profile: {profile_id}[/red]")
160+
raise typer.Exit(1)
161+
162+
console.print(f"[green]✓ Switched to profile:[/green] [cyan]{profile.name}[/cyan]")
163+
console.print(f"[dim]Email:[/dim] {profile.email}")
164+
console.print(f"[dim]API URL:[/dim] {profile.api_url}")
165+
166+
# Clear the settings cache so the new profile takes effect
167+
get_settings.cache_clear()
168+
169+
console.print("\n[yellow]Note:[/yellow] Profile switched successfully. " "The CLI will now connect to the selected profile's API URL.")
170+
171+
except Exception as e:
172+
console.print(f"[red]Error switching profile: {str(e)}[/red]")
173+
raise typer.Exit(1)
174+
175+
176+
def profiles_create(
177+
data_file: Optional[Path] = typer.Argument(None, help="JSON file containing prompt data (interactive mode if not provided)"),
178+
) -> None:
179+
"""Create a new profile interactively.
180+
181+
Walks the user through creating a new profile by prompting for all required
182+
fields. The new profile will be created in an inactive state. After creation,
183+
you will be asked if you want to enable the new profile.
184+
"""
185+
console = get_console()
186+
187+
try:
188+
console.print("\n[bold cyan]Create New Profile[/bold cyan]")
189+
console.print("[dim]You will be prompted for profile information.[/dim]\n")
190+
191+
# Generate a 16-character random ID (matching desktop app format)
192+
alphabet = string.ascii_letters + string.digits
193+
profile_id = "".join(secrets.choice(alphabet) for _ in range(16))
194+
created_at = datetime.now()
195+
196+
# Pre-fill fields that should not be prompted
197+
prefilled = {
198+
"id": profile_id,
199+
"is_active": False,
200+
"created_at": created_at,
201+
"last_used": None,
202+
}
203+
204+
if data_file:
205+
if not data_file.exists():
206+
console.print(f"[red]File not found: {data_file}[/red]")
207+
raise typer.Exit(1)
208+
profile_data = json.loads(data_file.read_text())
209+
profile_data.update(prefilled)
210+
else:
211+
profile_data = prompt_for_schema(AuthProfile, prefilled=prefilled)
212+
213+
# Create the AuthProfile instance
214+
new_profile = AuthProfile.model_validate(profile_data)
215+
216+
# Load or create the profile store
217+
store = load_profile_store()
218+
if not store:
219+
store = ProfileStore(profiles={}, active_profile_id=None)
220+
221+
# Add the new profile to the store
222+
store.profiles[new_profile.id] = new_profile
223+
224+
# Save the profile store
225+
save_profile_store(store)
226+
227+
console.print("\n[green]✓ Profile created successfully![/green]")
228+
console.print(f"[dim]Profile ID:[/dim] {new_profile.id}")
229+
console.print(f"[dim]Name:[/dim] {new_profile.name}")
230+
console.print(f"[dim]Email:[/dim] {new_profile.email}")
231+
console.print(f"[dim]API URL:[/dim] {new_profile.api_url}")
232+
233+
# Ask if the user wants to enable the new profile
234+
console.print("\n[yellow]Enable this profile now?[/yellow]", end=" ")
235+
if typer.confirm("", default=False):
236+
success = set_active_profile(new_profile.id)
237+
if success:
238+
console.print(f"[green]✓ Profile enabled:[/green] [cyan]{new_profile.name}[/cyan]")
239+
# Clear the settings cache so the new profile takes effect
240+
get_settings.cache_clear()
241+
else:
242+
console.print(f"[red]Failed to enable profile: {new_profile.id}[/red]")
243+
else:
244+
console.print("[dim]Profile created but not enabled. Use 'cforge profiles switch' to enable it later.[/dim]")
245+
246+
except Exception as e:
247+
console.print(f"[red]Error creating profile: {str(e)}[/red]")
248+
raise typer.Exit(1)

cforge/commands/settings/whoami.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,33 @@
99

1010
# First-Party
1111
from cforge.common import get_console, get_settings, get_token_file, load_token
12+
from cforge.profile_utils import get_active_profile
1213

1314

1415
def whoami() -> None:
1516
"""Show current authentication status and token source.
1617
17-
Displays where the authentication token is coming from (if any).
18+
Displays where the authentication token is coming from (if any) and
19+
information about the active profile if one is set.
1820
"""
21+
1922
console = get_console()
2023
settings = get_settings()
2124
env_token = settings.mcpgateway_bearer_token
2225
stored_token = load_token()
26+
active_profile = get_active_profile()
27+
28+
# Display active profile information
29+
console.print("[bold cyan]Active Profile:[/bold cyan]")
30+
console.print(f" [cyan]Name:[/cyan] {active_profile.name}")
31+
console.print(f" [cyan]ID:[/cyan] {active_profile.id}")
32+
console.print(f" [cyan]Email:[/cyan] {active_profile.email}")
33+
console.print(f" [cyan]API URL:[/cyan] {active_profile.api_url}")
34+
if active_profile.metadata and active_profile.metadata.environment:
35+
console.print(f" [cyan]Environment:[/cyan] {active_profile.metadata.environment}")
36+
console.print()
2337

38+
# Display authentication status
2439
if env_token:
2540
console.print("[green]✓ Authenticated via MCPGATEWAY_BEARER_TOKEN environment variable[/green]")
2641
console.print(f"[cyan]Token:[/cyan] {env_token[:10]}...")

0 commit comments

Comments
 (0)