Skip to content

Commit 715d4a0

Browse files
authored
Explicit org selector in codegen profile and codegen login (#1208)
1 parent b433eec commit 715d4a0

File tree

7 files changed

+428
-95
lines changed

7 files changed

+428
-95
lines changed

src/codegen/cli/auth/login.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import typer
55

66
from codegen.cli.api.webapp_routes import USER_SECRETS_ROUTE
7-
from codegen.cli.auth.token_manager import TokenManager
7+
from codegen.cli.auth.token_manager import TokenManager, get_cached_organizations, set_default_organization
88
from codegen.cli.env.global_env import global_env
99
from codegen.cli.errors import AuthError
10+
from codegen.cli.utils.simple_selector import simple_org_selector
1011

1112

1213
def login_routine(token: str | None = None) -> str:
@@ -27,20 +28,47 @@ def login_routine(token: str | None = None) -> str:
2728

2829
# If no token provided, guide user through browser flow
2930
if not token:
30-
rich.print(f"Opening {USER_SECRETS_ROUTE} to get your authentication token...")
3131
webbrowser.open_new(USER_SECRETS_ROUTE)
32-
token = typer.prompt("Please enter your authentication token from the browser", hide_input=False)
32+
token = typer.prompt(f"Enter your token from {USER_SECRETS_ROUTE}", hide_input=False)
3333

3434
if not token:
3535
rich.print("[red]Error:[/red] Token must be provided via CODEGEN_USER_ACCESS_TOKEN environment variable or manual input")
3636
raise typer.Exit(1)
3737

3838
# Validate and store token
3939
try:
40-
rich.print("[blue]Validating token and fetching account info...[/blue]")
4140
token_manager = TokenManager()
4241
token_manager.authenticate_token(token)
4342
rich.print(f"[green]✓ Stored token and profile to:[/green] {token_manager.token_file}")
43+
44+
# Show organization selector if multiple organizations available
45+
organizations = get_cached_organizations()
46+
if organizations and len(organizations) > 1:
47+
rich.print("\n[blue]Multiple organizations found. Please select your default:[/blue]")
48+
selected_org = simple_org_selector(organizations, title="🏢 Select Default Organization")
49+
50+
if selected_org:
51+
org_id = selected_org.get("id")
52+
org_name = selected_org.get("name")
53+
try:
54+
set_default_organization(org_id, org_name)
55+
rich.print(f"[green]✓ Set default organization:[/green] {org_name}")
56+
except Exception as e:
57+
rich.print(f"[yellow]Warning: Could not set default organization: {e}[/yellow]")
58+
rich.print("[yellow]You can set it later with 'codegen profile'[/yellow]")
59+
else:
60+
rich.print("[yellow]No organization selected. You can set it later with 'codegen profile'[/yellow]")
61+
elif organizations and len(organizations) == 1:
62+
# Single organization - set it automatically
63+
org = organizations[0]
64+
org_id = org.get("id")
65+
org_name = org.get("name")
66+
try:
67+
set_default_organization(org_id, org_name)
68+
rich.print(f"[green]✓ Set default organization:[/green] {org_name}")
69+
except Exception as e:
70+
rich.print(f"[yellow]Warning: Could not set default organization: {e}[/yellow]")
71+
4472
return token
4573
except AuthError as e:
4674
rich.print(f"[red]Error:[/red] {e!s}")

src/codegen/cli/auth/token_manager.py

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,7 @@ def save_token_with_org_info(self, token: str) -> None:
6161
# Store ALL organizations in cache for local resolution
6262
all_orgs = [{"id": org.get("id"), "name": org.get("name")} for org in orgs]
6363
primary_org = orgs[0] # Use first org as primary/default
64-
auth_data["organization"] = {
65-
"id": primary_org.get("id"),
66-
"name": primary_org.get("name"),
67-
"all_orgs": all_orgs
68-
}
64+
auth_data["organization"] = {"id": primary_org.get("id"), "name": primary_org.get("name"), "all_orgs": all_orgs}
6965
auth_data["organizations_cache"] = all_orgs # Separate cache for easy access
7066

7167
except requests.RequestException as e:
@@ -180,7 +176,7 @@ def get_user_info(self) -> dict | None:
180176

181177
def get_cached_organizations(self) -> list[dict] | None:
182178
"""Get all cached organizations.
183-
179+
184180
Returns:
185181
List of organization dictionaries with 'id' and 'name' keys, or None if no cache.
186182
"""
@@ -194,37 +190,75 @@ def get_cached_organizations(self) -> list[dict] | None:
194190

195191
def is_org_id_in_cache(self, org_id: int) -> bool:
196192
"""Check if an organization ID exists in the local cache.
197-
193+
198194
Args:
199195
org_id: The organization ID to check
200-
196+
201197
Returns:
202198
True if the organization ID is found in cache, False otherwise.
203199
"""
204200
cached_orgs = self.get_cached_organizations()
205201
if not cached_orgs:
206202
return False
207-
203+
208204
return any(org.get("id") == org_id for org in cached_orgs)
209205

210206
def get_org_name_from_cache(self, org_id: int) -> str | None:
211207
"""Get organization name from cache by ID.
212-
208+
213209
Args:
214210
org_id: The organization ID to look up
215-
211+
216212
Returns:
217213
Organization name if found in cache, None otherwise.
218214
"""
219215
cached_orgs = self.get_cached_organizations()
220216
if not cached_orgs:
221217
return None
222-
218+
223219
for org in cached_orgs:
224220
if org.get("id") == org_id:
225221
return org.get("name")
226222
return None
227223

224+
def set_default_organization(self, org_id: int, org_name: str) -> None:
225+
"""Set the default organization in auth.json.
226+
227+
Args:
228+
org_id: The organization ID to set as default
229+
org_name: The organization name
230+
"""
231+
auth_data = self.get_auth_data()
232+
if not auth_data:
233+
msg = "No authentication data found. Please run 'codegen login' first."
234+
raise ValueError(msg)
235+
236+
# Verify the org exists in cache
237+
if not self.is_org_id_in_cache(org_id):
238+
msg = f"Organization {org_id} not found in cache. Please run 'codegen login' to refresh."
239+
raise ValueError(msg)
240+
241+
# Update the organization info
242+
auth_data["organization"] = {"id": org_id, "name": org_name, "all_orgs": auth_data.get("organization", {}).get("all_orgs", [])}
243+
244+
# Save to file
245+
try:
246+
import json
247+
248+
with open(self.token_file, "w") as f:
249+
json.dump(auth_data, f, indent=2)
250+
251+
# Secure the file permissions (read/write for owner only)
252+
os.chmod(self.token_file, 0o600)
253+
254+
# Invalidate cache
255+
global _token_cache, _cache_mtime
256+
_token_cache = None
257+
_cache_mtime = None
258+
except Exception as e:
259+
msg = f"Error saving default organization: {e}"
260+
raise ValueError(msg)
261+
228262

229263
def get_current_token() -> str | None:
230264
"""Get the current authentication token if one exists.
@@ -289,7 +323,7 @@ def get_current_org_name() -> str | None:
289323

290324
def get_cached_organizations() -> list[dict] | None:
291325
"""Get all cached organizations.
292-
326+
293327
Returns:
294328
List of organization dictionaries with 'id' and 'name' keys, or None if no cache.
295329
"""
@@ -299,10 +333,10 @@ def get_cached_organizations() -> list[dict] | None:
299333

300334
def is_org_id_cached(org_id: int) -> bool:
301335
"""Check if an organization ID exists in the local cache.
302-
336+
303337
Args:
304338
org_id: The organization ID to check
305-
339+
306340
Returns:
307341
True if the organization ID is found in cache, False otherwise.
308342
"""
@@ -312,10 +346,10 @@ def is_org_id_cached(org_id: int) -> bool:
312346

313347
def get_org_name_from_cache(org_id: int) -> str | None:
314348
"""Get organization name from cache by ID.
315-
349+
316350
Args:
317351
org_id: The organization ID to look up
318-
352+
319353
Returns:
320354
Organization name if found in cache, None otherwise.
321355
"""
@@ -335,9 +369,10 @@ def get_current_user_info() -> dict | None:
335369

336370
# Repository caching functions (similar to organization caching)
337371

372+
338373
def get_cached_repositories() -> list[dict] | None:
339374
"""Get all cached repositories.
340-
375+
341376
Returns:
342377
List of repository dictionaries with 'id' and 'name' keys, or None if no cache.
343378
"""
@@ -350,7 +385,7 @@ def get_cached_repositories() -> list[dict] | None:
350385

351386
def cache_repositories(repositories: list[dict]) -> None:
352387
"""Cache repositories to local storage.
353-
388+
354389
Args:
355390
repositories: List of repository dictionaries to cache
356391
"""
@@ -361,53 +396,65 @@ def cache_repositories(repositories: list[dict]) -> None:
361396
# Save back to file
362397
try:
363398
import json
364-
with open(token_manager.token_file, 'w') as f:
399+
400+
with open(token_manager.token_file, "w") as f:
365401
json.dump(auth_data, f, indent=2)
366402
except Exception:
367403
pass # Fail silently
368404

369405

370406
def is_repo_id_cached(repo_id: int) -> bool:
371407
"""Check if a repository ID exists in the local cache.
372-
408+
373409
Args:
374410
repo_id: The repository ID to check
375-
411+
376412
Returns:
377413
True if the repository ID is found in cache, False otherwise.
378414
"""
379415
cached_repos = get_cached_repositories()
380416
if not cached_repos:
381417
return False
382-
418+
383419
return any(repo.get("id") == repo_id for repo in cached_repos)
384420

385421

386422
def get_repo_name_from_cache(repo_id: int) -> str | None:
387423
"""Get repository name from cache by ID.
388-
424+
389425
Args:
390426
repo_id: The repository ID to look up
391-
427+
392428
Returns:
393429
Repository name if found in cache, None otherwise.
394430
"""
395431
cached_repos = get_cached_repositories()
396432
if not cached_repos:
397433
return None
398-
434+
399435
for repo in cached_repos:
400436
if repo.get("id") == repo_id:
401437
return repo.get("name")
402-
438+
403439
return None
404440

405441

406442
def get_current_repo_name() -> str | None:
407443
"""Get the current repository name from environment or cache."""
408444
from codegen.cli.utils.repo import get_current_repo_id
409-
445+
410446
repo_id = get_current_repo_id()
411447
if repo_id:
412448
return get_repo_name_from_cache(repo_id)
413449
return None
450+
451+
452+
def set_default_organization(org_id: int, org_name: str) -> None:
453+
"""Set the default organization in auth.json.
454+
455+
Args:
456+
org_id: The organization ID to set as default
457+
org_name: The organization name
458+
"""
459+
token_manager = TokenManager()
460+
return token_manager.set_default_organization(org_id, org_name)

src/codegen/cli/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from codegen.cli.commands.login.main import login
1616
from codegen.cli.commands.logout.main import logout
1717
from codegen.cli.commands.org.main import org
18-
from codegen.cli.commands.profile.main import profile
18+
from codegen.cli.commands.profile.main import profile_app
1919
from codegen.cli.commands.repo.main import repo
2020
from codegen.cli.commands.style_debug.main import style_debug
2121
from codegen.cli.commands.tools.main import tools
@@ -42,7 +42,7 @@ def version_callback(value: bool):
4242
main.command("login", help="Store authentication token.")(login)
4343
main.command("logout", help="Clear stored authentication token.")(logout)
4444
main.command("org", help="Manage and switch between organizations.")(org)
45-
main.command("profile", help="Display information about the currently authenticated user.")(profile)
45+
# Profile is now a Typer app
4646
main.command("repo", help="Manage repository configuration and environment variables.")(repo)
4747
main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug)
4848
main.command("tools", help="List available tools from the Codegen API.")(tools)
@@ -53,6 +53,7 @@ def version_callback(value: bool):
5353
main.add_typer(agents_app, name="agents")
5454
main.add_typer(config_command, name="config")
5555
main.add_typer(integrations_app, name="integrations")
56+
main.add_typer(profile_app, name="profile")
5657

5758

5859
@main.callback(invoke_without_command=True)

src/codegen/cli/commands/login/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import rich
21
import typer
32

43
from codegen.cli.auth.login import login_routine
@@ -9,6 +8,6 @@ def login(token: str | None = typer.Option(None, help="API token for authenticat
98
"""Store authentication token."""
109
# Check if already authenticated
1110
if get_current_token():
12-
rich.print("[yellow]Info:[/yellow] You already have a token stored. Proceeding with re-authentication...")
11+
pass # Just proceed silently with re-authentication
1312

1413
login_routine(token)

0 commit comments

Comments
 (0)