llamactl 0.3.0a19__py3-none-any.whl → 0.3.0a21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,43 @@
1
1
  import asyncio
2
+ import json
3
+ import os
4
+ import platform
5
+ import subprocess
6
+ import sys
7
+ import webbrowser
2
8
 
3
9
  import click
10
+ import httpx
4
11
  import questionary
5
- from llama_deploy.cli.client import get_control_plane_client
12
+ from llama_deploy.cli.auth.client import (
13
+ DeviceAuthorizationRequest,
14
+ OIDCClient,
15
+ PlatformAuthClient,
16
+ PlatformAuthDiscoveryClient,
17
+ TokenRequestDeviceCode,
18
+ decode_jwt_claims,
19
+ decode_jwt_claims_from_device_oidc,
20
+ )
21
+ from llama_deploy.cli.config._config import ConfigManager
6
22
  from llama_deploy.cli.config.auth_service import AuthService
7
23
  from llama_deploy.cli.config.env_service import service
8
- from llama_deploy.core.client.manage_client import ClientError, ControlPlaneClient
24
+ from llama_deploy.cli.styles import (
25
+ ACTIVE_INDICATOR,
26
+ HEADER_COLOR,
27
+ MUTED_COL,
28
+ PRIMARY_COL,
29
+ WARNING,
30
+ )
31
+ from llama_deploy.core.client.manage_client import (
32
+ ControlPlaneClient,
33
+ )
9
34
  from llama_deploy.core.schema.projects import ProjectSummary
10
35
  from rich import print as rprint
11
36
  from rich.table import Table
12
37
  from rich.text import Text
13
38
 
14
39
  from ..app import app, console
15
- from ..config.schema import Auth
40
+ from ..config.schema import Auth, DeviceOIDC
16
41
  from ..options import global_options, interactive_option
17
42
 
18
43
 
@@ -61,14 +86,14 @@ def create_api_key_profile(
61
86
 
62
87
  # Interactive mode: prompt for token (masked) and validate
63
88
  token_value = api_key or _prompt_for_api_key()
64
- projects = _validate_token_and_list_projects(auth_svc, token_value)
89
+ projects = _prompt_validate_api_key_and_list_projects(auth_svc, token_value)
65
90
 
66
91
  # Select or enter project ID
67
92
  selected_project_id = project_id or _select_or_enter_project(
68
93
  projects, auth_svc.env.requires_auth
69
94
  )
70
95
  if not selected_project_id:
71
- rprint("[yellow]No project selected[/yellow]")
96
+ rprint(f"[{WARNING}]No project selected[/]")
72
97
  return
73
98
 
74
99
  # Create and set profile
@@ -81,28 +106,47 @@ def create_api_key_profile(
81
106
  raise click.Abort()
82
107
 
83
108
 
109
+ @auth.command("login")
110
+ @global_options
111
+ def device_login() -> None:
112
+ """Login via web browser"""
113
+
114
+ try:
115
+ created = _create_device_profile()
116
+ rprint(
117
+ f"[green]Created login profile '{created.name}' and set as current[/green]"
118
+ )
119
+
120
+ except Exception as e:
121
+ rprint(f"[red]Error: {e}[/red]")
122
+ raise click.Abort()
123
+
124
+
84
125
  @auth.command("list")
85
126
  @global_options
86
127
  def list_profiles() -> None:
87
- """List all logged in profiles"""
128
+ """List all logged in users/tokens"""
88
129
  try:
89
130
  auth_svc = service.current_auth_service()
90
131
  profiles = auth_svc.list_profiles()
91
132
  current = auth_svc.get_current_profile()
92
133
 
93
134
  if not profiles:
94
- rprint("[yellow]No profiles found[/yellow]")
95
- rprint("Create one with: [cyan]llamactl auth token[/cyan]")
135
+ rprint(f"[{WARNING}]No profiles found[/]")
136
+ if auth_svc.env.requires_auth:
137
+ rprint("Create one with: [cyan]llamactl auth login[/cyan]")
138
+ else:
139
+ rprint("Create one with: [cyan]llamactl auth token[/cyan]")
96
140
  return
97
141
 
98
- table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
99
- table.add_column(" Name")
100
- table.add_column("Active Project", style="grey46")
142
+ table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
143
+ table.add_column(" Name", style=PRIMARY_COL)
144
+ table.add_column("Active Project", style=MUTED_COL)
101
145
 
102
146
  for profile in profiles:
103
147
  text = Text()
104
148
  if profile == current:
105
- text.append("* ", style="magenta")
149
+ text.append("* ", style=ACTIVE_INDICATOR)
106
150
  else:
107
151
  text.append(" ")
108
152
  text.append(profile.name)
@@ -119,6 +163,26 @@ def list_profiles() -> None:
119
163
  raise click.Abort()
120
164
 
121
165
 
166
+ @auth.command("destroy", hidden=True)
167
+ @global_options
168
+ def destroy_database() -> None:
169
+ """Destroy the database"""
170
+ if not questionary.confirm(
171
+ "Are you sure you want to destroy all of your local logins? This action cannot be undone."
172
+ ).ask():
173
+ return
174
+ ConfigManager(init_database=False).destroy_database()
175
+ rprint("[green]Database destroyed[/green]")
176
+
177
+
178
+ @auth.command("show-db", hidden=True)
179
+ @global_options
180
+ def config_database() -> None:
181
+ """Config the database"""
182
+ path = service.config_manager().db_path
183
+ rprint(f"[bold]{path}[/bold]")
184
+
185
+
122
186
  @auth.command("switch")
123
187
  @global_options
124
188
  @click.argument("name", required=False)
@@ -129,7 +193,7 @@ def switch_profile(name: str | None, interactive: bool) -> None:
129
193
  try:
130
194
  selected_auth = _select_profile(auth_svc, name, interactive)
131
195
  if not selected_auth:
132
- rprint("[yellow]No profile selected[/yellow]")
196
+ rprint(f"[{WARNING}]No profile selected[/]")
133
197
  return
134
198
 
135
199
  auth_svc.set_current_profile(selected_auth.name)
@@ -150,10 +214,10 @@ def delete_profile(name: str | None, interactive: bool) -> None:
150
214
  auth_svc = service.current_auth_service()
151
215
  auth = _select_profile(auth_svc, name, interactive)
152
216
  if not auth:
153
- rprint("[yellow]No profile selected[/yellow]")
217
+ rprint(f"[{WARNING}]No profile selected[/]")
154
218
  return
155
219
 
156
- if auth_svc.delete_profile(auth.name):
220
+ if asyncio.run(auth_svc.delete_profile(auth.name)):
157
221
  rprint(f"[green]Logged out from '{auth.name}'[/green]")
158
222
  else:
159
223
  rprint(f"[red]Profile '{auth.name}' not found[/red]")
@@ -163,6 +227,29 @@ def delete_profile(name: str | None, interactive: bool) -> None:
163
227
  raise click.Abort()
164
228
 
165
229
 
230
+ # Very simple introspection: decode current token via provider JWKS
231
+ @auth.command("me", hidden=True)
232
+ @global_options
233
+ def me() -> None:
234
+ """Print JWT claims for the current profile's token using provider JWKS.
235
+
236
+ Assumes the stored API key is a JWT (e.g., OIDC id_token).
237
+ """
238
+ try:
239
+ auth_svc = service.current_auth_service()
240
+ profile = auth_svc.get_current_profile()
241
+ if not profile or not profile.device_oidc:
242
+ raise click.ClickException(
243
+ "No OIDC profile selected. Run `llamactl auth login` or switch to an existing OIDC profile."
244
+ )
245
+
246
+ claims = asyncio.run(decode_jwt_claims_from_device_oidc(profile.device_oidc))
247
+ click.echo(json.dumps(claims, indent=2, sort_keys=True))
248
+ except Exception as e:
249
+ rprint(f"[red]Error: {e}[/red]")
250
+ raise click.Abort()
251
+
252
+
166
253
  # Projects commands
167
254
  @auth.command("project")
168
255
  @click.argument("project_id", required=False)
@@ -175,19 +262,25 @@ def change_project(project_id: str | None, interactive: bool) -> None:
175
262
  return
176
263
  auth_svc = service.current_auth_service()
177
264
  if project_id:
265
+ if auth_svc.env.requires_auth:
266
+ projects = _list_projects(auth_svc)
267
+ if not next(
268
+ (project for project in projects if project.project_id == project_id),
269
+ None,
270
+ ):
271
+ raise click.ClickException(f"Project {project_id} not found")
178
272
  auth_svc.set_project(profile.name, project_id)
179
- rprint(f"[green]Set active project to '{project_id}'[/green]")
273
+ rprint(f"Set active project to [bold green]{project_id}[/]")
180
274
  return
181
275
  if not interactive:
182
276
  raise click.ClickException(
183
277
  "No --project-id provided. Run `llamactl auth project --help` for more information."
184
278
  )
185
279
  try:
186
- client = get_control_plane_client()
187
- projects = asyncio.run(client.list_projects())
280
+ projects = _list_projects(auth_svc)
188
281
 
189
282
  if not projects:
190
- rprint("[yellow]No projects found[/yellow]")
283
+ rprint(f"[{WARNING}]No projects found[/]")
191
284
  return
192
285
  result = questionary.select(
193
286
  "Select a project",
@@ -208,15 +301,173 @@ def change_project(project_id: str | None, interactive: bool) -> None:
208
301
  project_id = questionary.text("Enter project ID").ask()
209
302
  result = project_id
210
303
  if result:
304
+ selected_project = next(
305
+ (project for project in projects if project.project_id == result), None
306
+ )
307
+ name = selected_project.project_name if selected_project else result
211
308
  auth_svc.set_project(profile.name, result)
212
- rprint(f"[green]Set active project to '{result}'[/green]")
309
+ rprint(f"Set active project to [bold {PRIMARY_COL}]{name}[/]")
213
310
  else:
214
- rprint("[yellow]No project selected[/yellow]")
311
+ rprint(f"[{WARNING}]No project selected[/]")
215
312
  except Exception as e:
216
313
  rprint(f"[red]Error: {e}[/red]")
217
314
  raise click.Abort()
218
315
 
219
316
 
317
+ def _auto_device_name() -> str:
318
+ try:
319
+ if sys.platform == "darwin": # macOS
320
+ return (
321
+ subprocess.check_output(["scutil", "--get", "ComputerName"])
322
+ .decode()
323
+ .strip()
324
+ )
325
+ elif sys.platform.startswith("win"):
326
+ return os.environ["COMPUTERNAME"]
327
+ else: # Linux / Unix
328
+ return platform.node()
329
+ except Exception:
330
+ return platform.node()
331
+
332
+
333
+ async def _create_or_update_agent_api_key(auth_svc: AuthService, profile: Auth) -> None:
334
+ """
335
+ Mutates and updates the profile with an agent API key if it does not exist or is invalid.
336
+ """
337
+ if profile.api_key is not None:
338
+ async with PlatformAuthClient(profile.api_url, profile.api_key) as client:
339
+ try:
340
+ await client.me()
341
+ except httpx.HTTPStatusError as e:
342
+ if e.response.status_code == 401:
343
+ # must have been deleted
344
+ profile.api_key = None
345
+ profile.api_key_id = None
346
+ else:
347
+ raise
348
+ if profile.api_key is None:
349
+ async with auth_svc.profile_client(profile) as client:
350
+ name = f"{profile.name} llamactl on {profile.device_oidc.device_name if profile.device_oidc else 'unknown'}"
351
+ api_key = await client.create_agent_api_key(name)
352
+ profile.api_key = api_key.token
353
+ profile.api_key_id = api_key.id
354
+ auth_svc.update_profile(profile)
355
+
356
+
357
+ def _create_device_profile() -> Auth:
358
+ auth_svc = service.current_auth_service()
359
+ if not auth_svc.env.requires_auth:
360
+ raise click.ClickException("This environment does not support authentication")
361
+
362
+ base_url = auth_svc.env.api_url.rstrip("/")
363
+
364
+ oidc_device = asyncio.run(_run_device_authentication(base_url))
365
+
366
+ # Obtain or prompt for project ID and create profile
367
+ projects = _list_projects(auth_svc, oidc_device.device_access_token)
368
+ selected_project_id = _select_or_enter_project(projects, True)
369
+ if not selected_project_id:
370
+ raise click.ClickException(
371
+ "Project is required. Does this user have access to any projects?"
372
+ )
373
+ created = auth_svc.create_or_update_profile_from_oidc(
374
+ selected_project_id, oidc_device
375
+ )
376
+ asyncio.run(_create_or_update_agent_api_key(auth_svc, created))
377
+ return created
378
+
379
+
380
+ async def _run_device_authentication(base_url: str) -> DeviceOIDC:
381
+ device_name = _auto_device_name()
382
+ # 1) Discover upstream and CLI client_id via client
383
+ async with PlatformAuthDiscoveryClient(base_url) as discovery:
384
+ disc = await discovery.oidc_discovery()
385
+ upstream = disc.discovery_url
386
+ client_ids = disc.client_ids or {}
387
+ client_ids_list = list(client_ids.values())
388
+ client_id = client_ids.get("cli") or (
389
+ client_ids_list[0] if len(client_ids_list) == 1 else None
390
+ )
391
+ if not client_id:
392
+ raise click.ClickException(
393
+ "Expected 'cli' Client ID not found from auth discovery"
394
+ )
395
+
396
+ # 2) Device flow via typed OIDC client
397
+ async with OIDCClient() as oidc:
398
+ provider = await oidc.fetch_provider_configuration(upstream)
399
+ device_endpoint = provider.device_authorization_endpoint
400
+ token_endpoint = provider.token_endpoint
401
+ if not device_endpoint or not token_endpoint:
402
+ raise click.ClickException("Device Authorization not supported by provider")
403
+
404
+ scope_value = " ".join(sorted({"openid", "profile", "email", "offline_access"}))
405
+
406
+ # 3) Start device authorization
407
+ da = await oidc.device_authorization(
408
+ device_endpoint,
409
+ DeviceAuthorizationRequest(client_id=client_id, scope=scope_value),
410
+ )
411
+
412
+ rprint(
413
+ "[bold]complete authentication by visiting the verification URI and confirming the device:[/bold]"
414
+ )
415
+ if da.verification_uri:
416
+ rprint(
417
+ f"Verification URI: {da.verification_uri} (will open in your browser if supported)"
418
+ )
419
+ if da.user_code:
420
+ rprint(f"User Code: {da.user_code} to confirm the device")
421
+ if da.verification_uri_complete:
422
+ try:
423
+ webbrowser.open(da.verification_uri_complete)
424
+ except Exception:
425
+ pass
426
+
427
+ # 4) Poll token endpoint
428
+ interval = int(da.interval or 5)
429
+ while True:
430
+ await asyncio.sleep(interval)
431
+ token = await oidc.token_with_device_code(
432
+ token_endpoint,
433
+ TokenRequestDeviceCode(
434
+ device_code=da.device_code,
435
+ client_id=client_id,
436
+ ),
437
+ )
438
+ if token.error in {"authorization_pending", "slow_down"}:
439
+ if token.error == "slow_down":
440
+ interval += 5
441
+ continue
442
+ if token.error:
443
+ raise click.ClickException(
444
+ f"Token polling failed: {token.error} {token.error_description or ''}"
445
+ )
446
+ if token.id_token:
447
+ if not token.access_token:
448
+ raise click.ClickException(
449
+ "Device flow failed: token response missing access_token"
450
+ )
451
+ claims = await decode_jwt_claims(token.id_token, provider.jwks_uri)
452
+ email = claims.get("email")
453
+ if not email:
454
+ raise click.ClickException(
455
+ "Device flow failed: email not found in token"
456
+ )
457
+ user_id = claims.get("sub") or email
458
+ return DeviceOIDC(
459
+ device_name=device_name,
460
+ email=email,
461
+ user_id=user_id,
462
+ client_id=client_id,
463
+ discovery_url=upstream,
464
+ device_access_token=token.access_token,
465
+ device_refresh_token=token.refresh_token,
466
+ device_id_token=token.id_token,
467
+ )
468
+ raise click.ClickException("Device flow failed: unexpected token response")
469
+
470
+
220
471
  def validate_authenticated_profile(interactive: bool) -> Auth:
221
472
  """Validate that the user is authenticated within the current environment.
222
473
 
@@ -259,7 +510,7 @@ def validate_authenticated_profile(interactive: bool) -> Auth:
259
510
  # No profiles exist for this env
260
511
  if current_env.requires_auth:
261
512
  # Inline token flow
262
- created = _token_flow_for_env(auth_svc)
513
+ created = _create_device_profile()
263
514
  return created
264
515
  else:
265
516
  # No auth required: select project and create a default profile without token
@@ -282,22 +533,38 @@ def _prompt_for_api_key() -> str:
282
533
  raise click.ClickException("No API key entered")
283
534
 
284
535
 
285
- def _validate_token_and_list_projects(
286
- auth_svc: AuthService, api_key: str
536
+ def _list_projects(
537
+ auth_svc: AuthService,
538
+ api_key: str | None = None,
287
539
  ) -> list[ProjectSummary]:
288
540
  async def _run():
289
- async with ControlPlaneClient.ctx(auth_svc.env.api_url, api_key) as client:
541
+ profile = auth_svc.get_current_profile()
542
+ async with ControlPlaneClient.ctx(
543
+ auth_svc.env.api_url,
544
+ api_key or (profile.api_key if profile else None),
545
+ None if api_key is not None else auth_svc.auth_middleware(profile),
546
+ ) as client:
290
547
  return await client.list_projects()
291
548
 
549
+ return asyncio.run(_run())
550
+
551
+
552
+ def _prompt_validate_api_key_and_list_projects(
553
+ auth_svc: AuthService, api_key: str
554
+ ) -> list[ProjectSummary]:
292
555
  try:
293
- return asyncio.run(_run())
294
- except ClientError as e:
295
- if getattr(e, "status_code", None) == 401:
556
+ return _list_projects(auth_svc, api_key)
557
+ except httpx.HTTPStatusError as e:
558
+ if e.response.status_code == 401:
296
559
  rprint("[red]Invalid API key. Please try again.[/red]")
297
- return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
298
- if getattr(e, "status_code", None) == 403:
560
+ return _prompt_validate_api_key_and_list_projects(
561
+ auth_svc, _prompt_for_api_key()
562
+ )
563
+ if e.response.status_code == 403:
299
564
  rprint("[red]This environment requires a valid API key.[/red]")
300
- return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
565
+ return _prompt_validate_api_key_and_list_projects(
566
+ auth_svc, _prompt_for_api_key()
567
+ )
301
568
  raise
302
569
  except Exception as e:
303
570
  raise click.ClickException(f"Failed to validate API key: {e}")
@@ -327,7 +594,7 @@ def _select_or_enter_project(
327
594
 
328
595
  def _token_flow_for_env(auth_service: AuthService) -> Auth:
329
596
  token_value = _prompt_for_api_key()
330
- projects = _validate_token_and_list_projects(auth_service, token_value)
597
+ projects = _prompt_validate_api_key_and_list_projects(auth_service, token_value)
331
598
  project_id = _select_or_enter_project(projects, auth_service.env.requires_auth)
332
599
  if not project_id:
333
600
  raise click.ClickException("No project selected")
@@ -357,7 +624,7 @@ def _select_profile(
357
624
  profiles = auth_svc.list_profiles()
358
625
 
359
626
  if not profiles:
360
- rprint("[yellow]No profiles found[/yellow]")
627
+ rprint(f"[{WARNING}]No profiles found[/]")
361
628
  return None
362
629
 
363
630
  choices = []
@@ -11,6 +11,7 @@ import asyncio
11
11
  import click
12
12
  import questionary
13
13
  from llama_deploy.cli.commands.auth import validate_authenticated_profile
14
+ from llama_deploy.cli.styles import HEADER_COLOR, MUTED_COL, PRIMARY_COL, WARNING
14
15
  from llama_deploy.core.schema.deployments import DeploymentUpdate
15
16
  from rich import print as rprint
16
17
  from rich.table import Table
@@ -52,15 +53,15 @@ def list_deployments(interactive: bool) -> None:
52
53
 
53
54
  if not deployments:
54
55
  rprint(
55
- f"[yellow]No deployments found for project {client.project_id}[/yellow]"
56
+ f"[{WARNING}]No deployments found for project {client.project_id}[/]"
56
57
  )
57
58
  return
58
59
 
59
- table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
60
- table.add_column("Name")
61
- table.add_column("Status", style="grey46")
62
- table.add_column("URL", style="grey46")
63
- table.add_column("Repository", style="grey46")
60
+ table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
61
+ table.add_column("Name", style=PRIMARY_COL)
62
+ table.add_column("Status", style=MUTED_COL)
63
+ table.add_column("URL", style=MUTED_COL)
64
+ table.add_column("Repository", style=MUTED_COL)
64
65
 
65
66
  for deployment in deployments:
66
67
  name = deployment.id
@@ -96,7 +97,7 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
96
97
 
97
98
  deployment_id = select_deployment(deployment_id)
98
99
  if not deployment_id:
99
- rprint("[yellow]No deployment selected[/yellow]")
100
+ rprint(f"[{WARNING}]No deployment selected[/]")
100
101
  return
101
102
  if interactive:
102
103
  monitor_deployment_screen(deployment_id)
@@ -104,9 +105,9 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
104
105
 
105
106
  deployment = asyncio.run(client.get_deployment(deployment_id))
106
107
 
107
- table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
108
- table.add_column("Property", style="grey46", justify="right")
109
- table.add_column("Value")
108
+ table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
109
+ table.add_column("Property", style=MUTED_COL, justify="right")
110
+ table.add_column("Value", style=PRIMARY_COL)
110
111
 
111
112
  table.add_row("ID", Text(deployment.id))
112
113
  table.add_row("Project ID", Text(deployment.project_id))
@@ -148,7 +149,7 @@ def create_deployment(
148
149
  # Use interactive creation
149
150
  deployment_form = create_deployment_form()
150
151
  if deployment_form is None:
151
- rprint("[yellow]Cancelled[/yellow]")
152
+ rprint(f"[{WARNING}]Cancelled[/]")
152
153
  return
153
154
 
154
155
  rprint(
@@ -168,12 +169,12 @@ def delete_deployment(deployment_id: str | None, interactive: bool) -> None:
168
169
 
169
170
  deployment_id = select_deployment(deployment_id, interactive=interactive)
170
171
  if not deployment_id:
171
- rprint("[yellow]No deployment selected[/yellow]")
172
+ rprint(f"[{WARNING}]No deployment selected[/]")
172
173
  return
173
174
 
174
175
  if interactive:
175
176
  if not confirm_action(f"Delete deployment '{deployment_id}'?"):
176
- rprint("[yellow]Cancelled[/yellow]")
177
+ rprint(f"[{WARNING}]Cancelled[/]")
177
178
  return
178
179
 
179
180
  asyncio.run(client.delete_deployment(deployment_id))
@@ -196,7 +197,7 @@ def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
196
197
 
197
198
  deployment_id = select_deployment(deployment_id, interactive=interactive)
198
199
  if not deployment_id:
199
- rprint("[yellow]No deployment selected[/yellow]")
200
+ rprint(f"[{WARNING}]No deployment selected[/]")
200
201
  return
201
202
 
202
203
  # Get current deployment details
@@ -205,7 +206,7 @@ def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
205
206
  # Use the interactive edit form
206
207
  updated_deployment = edit_deployment_form(current_deployment)
207
208
  if updated_deployment is None:
208
- rprint("[yellow]Cancelled[/yellow]")
209
+ rprint(f"[{WARNING}]Cancelled[/]")
209
210
  return
210
211
 
211
212
  rprint(
@@ -227,7 +228,7 @@ def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
227
228
  try:
228
229
  deployment_id = select_deployment(deployment_id)
229
230
  if not deployment_id:
230
- rprint("[yellow]No deployment selected[/yellow]")
231
+ rprint(f"[{WARNING}]No deployment selected[/]")
231
232
  return
232
233
 
233
234
  # Get current deployment details to show what we're refreshing
@@ -278,29 +279,22 @@ def select_deployment(
278
279
  if not interactive:
279
280
  return None
280
281
 
281
- try:
282
- client = get_project_client()
283
- deployments = asyncio.run(client.list_deployments())
282
+ client = get_project_client()
283
+ deployments = asyncio.run(client.list_deployments())
284
284
 
285
- if not deployments:
286
- rprint(
287
- f"[yellow]No deployments found for project {client.project_id}[/yellow]"
288
- )
289
- return None
285
+ if not deployments:
286
+ rprint(f"[{WARNING}]No deployments found for project {client.project_id}[/]")
287
+ return None
290
288
 
291
- choices = []
292
- for deployment in deployments:
293
- name = deployment.name
294
- deployment_id = deployment.id
295
- status = deployment.status
296
- choices.append(
297
- questionary.Choice(
298
- title=f"{name} ({deployment_id}) - {status}", value=deployment_id
299
- )
289
+ choices = []
290
+ for deployment in deployments:
291
+ name = deployment.name
292
+ deployment_id = deployment.id
293
+ status = deployment.status
294
+ choices.append(
295
+ questionary.Choice(
296
+ title=f"{name} ({deployment_id}) - {status}", value=deployment_id
300
297
  )
298
+ )
301
299
 
302
- return questionary.select("Select deployment:", choices=choices).ask()
303
-
304
- except Exception as e:
305
- rprint(f"[red]Error loading deployments: {e}[/red]")
306
- return None
300
+ return questionary.select("Select deployment:", choices=choices).ask()