Skip to content

Commit d0cf30b

Browse files
committed
feat: Add cforge profiles create
#13 Branch: Profiles-13 Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>
1 parent 5515d53 commit d0cf30b

File tree

3 files changed

+162
-1
lines changed

3 files changed

+162
-1
lines changed

cforge/commands/settings/profiles.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,26 @@
88
"""
99

1010
# Standard
11+
from datetime import datetime
1112
from typing import Optional
13+
import secrets
14+
import string
1215

1316
# Third-Party
1417
import typer
1518

1619
# First-Party
17-
from cforge.common import get_console, print_table, print_json
20+
from cforge.common import get_console, print_table, print_json, prompt_for_schema
1821
from cforge.config import get_settings
1922
from cforge.profile_utils import (
23+
AuthProfile,
24+
ProfileStore,
2025
get_all_profiles,
2126
get_profile,
2227
get_active_profile,
2328
set_active_profile,
29+
load_profile_store,
30+
save_profile_store,
2431
)
2532

2633

@@ -197,3 +204,70 @@ def profiles_current() -> None:
197204
except Exception as e:
198205
console.print(f"[red]Error retrieving current profile: {str(e)}[/red]")
199206
raise typer.Exit(1)
207+
208+
209+
def profiles_create() -> None:
210+
"""Create a new profile interactively.
211+
212+
Walks the user through creating a new profile by prompting for all required
213+
fields. The new profile will be created in an inactive state. After creation,
214+
you will be asked if you want to enable the new profile.
215+
"""
216+
console = get_console()
217+
218+
try:
219+
console.print("\n[bold cyan]Create New Profile[/bold cyan]")
220+
console.print("[dim]You will be prompted for profile information.[/dim]\n")
221+
222+
# Generate a 16-character random ID (matching desktop app format)
223+
alphabet = string.ascii_letters + string.digits
224+
profile_id = "".join(secrets.choice(alphabet) for _ in range(16))
225+
created_at = datetime.now()
226+
227+
# Pre-fill fields that should not be prompted
228+
prefilled = {
229+
"id": profile_id,
230+
"is_active": False,
231+
"created_at": created_at,
232+
"last_used": None,
233+
}
234+
235+
# Prompt for profile data using the schema
236+
profile_data = prompt_for_schema(AuthProfile, prefilled=prefilled)
237+
238+
# Create the AuthProfile instance
239+
new_profile = AuthProfile.model_validate(profile_data)
240+
241+
# Load or create the profile store
242+
store = load_profile_store()
243+
if not store:
244+
store = ProfileStore(profiles={}, active_profile_id=None)
245+
246+
# Add the new profile to the store
247+
store.profiles[new_profile.id] = new_profile
248+
249+
# Save the profile store
250+
save_profile_store(store)
251+
252+
console.print("\n[green]✓ Profile created successfully![/green]")
253+
console.print(f"[dim]Profile ID:[/dim] {new_profile.id}")
254+
console.print(f"[dim]Name:[/dim] {new_profile.name}")
255+
console.print(f"[dim]Email:[/dim] {new_profile.email}")
256+
console.print(f"[dim]API URL:[/dim] {new_profile.api_url}")
257+
258+
# Ask if the user wants to enable the new profile
259+
console.print("\n[yellow]Enable this profile now?[/yellow]", end=" ")
260+
if typer.confirm("", default=False):
261+
success = set_active_profile(new_profile.id)
262+
if success:
263+
console.print(f"[green]✓ Profile enabled:[/green] [cyan]{new_profile.name}[/cyan]")
264+
# Clear the settings cache so the new profile takes effect
265+
get_settings.cache_clear()
266+
else:
267+
console.print(f"[red]Failed to enable profile: {new_profile.id}[/red]")
268+
else:
269+
console.print("[dim]Profile created but not enabled. Use 'cforge profiles switch' to enable it later.[/dim]")
270+
271+
except Exception as e:
272+
console.print(f"[red]Error creating profile: {str(e)}[/red]")
273+
raise typer.Exit(1)

cforge/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
profiles_app.command("get")(profiles.profiles_get)
134134
profiles_app.command("switch")(profiles.profiles_switch)
135135
profiles_app.command("current")(profiles.profiles_current)
136+
profiles_app.command("create")(profiles.profiles_create)
136137

137138
# ---------------------------------------------------------------------------
138139
# Deploy command (hidden stub for future use)

tests/commands/settings/test_profiles.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
# First-Party
1919
from cforge.commands.settings.profiles import (
20+
profiles_create,
2021
profiles_current,
2122
profiles_get,
2223
profiles_list,
@@ -539,3 +540,88 @@ def test_profiles_current_no_environment(self, mock_console, mock_settings) -> N
539540
assert any("Current Profile" in call for call in print_calls)
540541
assert any("current@example.com" in call for call in print_calls)
541542
assert not any("Environment:" in call for call in print_calls)
543+
544+
545+
class TestProfilesCreate:
546+
"""Tests for profiles create command."""
547+
548+
def test_profiles_create_success(self, mock_console, mock_settings) -> None:
549+
"""Test successfully creating a new profile."""
550+
with patch("cforge.commands.settings.profiles.get_console", return_value=mock_console):
551+
with patch("cforge.commands.settings.profiles.prompt_for_schema") as mock_prompt:
552+
with patch("cforge.commands.settings.profiles.typer.confirm", return_value=False):
553+
# Mock the prompt to return profile data
554+
mock_prompt.return_value = {
555+
"id": "test-profile-id",
556+
"name": "Test Profile",
557+
"email": "test@example.com",
558+
"api_url": "https://api.test.com",
559+
"is_active": False,
560+
"created_at": datetime.now(),
561+
}
562+
563+
profiles_create()
564+
565+
# Verify success message
566+
print_calls = [str(call) for call in mock_console.print.call_args_list]
567+
assert any("Profile created successfully" in call for call in print_calls)
568+
assert any("Test Profile" in call for call in print_calls)
569+
570+
def test_profiles_create_and_enable(self, mock_console, mock_settings) -> None:
571+
"""Test creating a profile and enabling it."""
572+
with patch("cforge.commands.settings.profiles.get_console", return_value=mock_console):
573+
with patch("cforge.commands.settings.profiles.prompt_for_schema") as mock_prompt:
574+
with patch("cforge.commands.settings.profiles.typer.confirm", return_value=True):
575+
with patch("cforge.commands.settings.profiles.set_active_profile", return_value=True) as set_active_profile_mock:
576+
with patch("cforge.commands.settings.profiles.get_settings") as mock_get_settings:
577+
mock_get_settings.cache_clear = Mock()
578+
579+
# Mock the prompt to return profile data
580+
mock_prompt.return_value = {
581+
"id": "test-profile-id",
582+
"name": "Test Profile",
583+
"email": "test@example.com",
584+
"api_url": "https://api.test.com",
585+
"is_active": False,
586+
"created_at": datetime.now(),
587+
}
588+
589+
profiles_create()
590+
591+
# Verify success and enable messages
592+
print_calls = [str(call) for call in mock_console.print.call_args_list]
593+
assert any("Profile created successfully" in call for call in print_calls)
594+
assert any("Profile enabled" in call for call in print_calls)
595+
set_active_profile_mock.assert_called_with("test-profile-id")
596+
597+
def test_profiles_create_error(self, mock_console, mock_settings) -> None:
598+
"""Test creating profile with an error."""
599+
with patch("cforge.commands.settings.profiles.get_console", return_value=mock_console):
600+
with patch("cforge.commands.settings.profiles.prompt_for_schema", side_effect=Exception("Test error")):
601+
with pytest.raises(typer.Exit) as exc_info:
602+
profiles_create()
603+
604+
assert exc_info.value.exit_code == 1
605+
assert any("Error creating profile" in str(call) for call in mock_console.print.call_args_list)
606+
607+
def test_profiles_create_enable_fails(self, mock_console, mock_settings) -> None:
608+
"""Test creating profile but enabling fails."""
609+
with patch("cforge.commands.settings.profiles.get_console", return_value=mock_console):
610+
with patch("cforge.commands.settings.profiles.prompt_for_schema") as mock_prompt:
611+
with patch("cforge.commands.settings.profiles.typer.confirm", return_value=True):
612+
with patch("cforge.commands.settings.profiles.set_active_profile", return_value=False):
613+
# Mock the prompt to return profile data
614+
mock_prompt.return_value = {
615+
"id": "test-profile-id",
616+
"name": "Test Profile",
617+
"email": "test@example.com",
618+
"api_url": "https://api.test.com",
619+
"is_active": False,
620+
"created_at": datetime.now(),
621+
}
622+
623+
profiles_create()
624+
625+
# Verify failure message
626+
print_calls = [str(call) for call in mock_console.print.call_args_list]
627+
assert any("Failed to enable profile" in call for call in print_calls)

0 commit comments

Comments
 (0)