llamactl 0.3.0a13__py3-none-any.whl → 0.3.0a14__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.
@@ -3,20 +3,17 @@ import asyncio
3
3
  import click
4
4
  import questionary
5
5
  from llama_deploy.cli.client import get_control_plane_client
6
- from llama_deploy.cli.interactive_prompts.session_utils import is_interactive_session
6
+ from llama_deploy.cli.config.auth_service import AuthService
7
+ from llama_deploy.cli.config.env_service import service
8
+ from llama_deploy.core.client.manage_client import ClientError, ControlPlaneClient
9
+ from llama_deploy.core.schema.projects import ProjectSummary
7
10
  from rich import print as rprint
8
11
  from rich.table import Table
12
+ from rich.text import Text
9
13
 
10
14
  from ..app import app, console
11
- from ..config import Profile, config_manager
12
- from ..interactive_prompts.utils import (
13
- select_profile,
14
- )
15
+ from ..config.schema import Auth
15
16
  from ..options import global_options, interactive_option
16
- from ..textual.api_key_profile_form import (
17
- create_api_key_profile_form,
18
- edit_api_key_profile_form,
19
- )
20
17
 
21
18
 
22
19
  # Create sub-applications for organizing commands
@@ -30,156 +27,90 @@ def auth() -> None:
30
27
  pass
31
28
 
32
29
 
33
- # Profile commands
34
- @auth.command("login")
30
+ @auth.command("token")
35
31
  @global_options
36
- @click.option(
37
- "--api-url",
38
- help="Specify a custom control plane API URL to log into",
39
- default="https://api.cloud.llamaindex.ai",
40
- )
41
- @click.option(
42
- "--name",
43
- help="Specify a memorable name for the API key login when creating non-interactively",
44
- )
45
32
  @click.option(
46
33
  "--project-id",
47
34
  help="Project ID to use for the login when creating non-interactively",
48
35
  )
49
36
  @click.option(
50
37
  "--api-key",
51
- help="Advanced: Control plane/Llama Cloud Bearer API key. Only needed if control plane is authenticated",
52
- )
53
- @click.option(
54
- "--login-url", help="Advanced: Custom login URL for initiating OpenID Connect flow"
38
+ help="API key to use for the login when creating non-interactively",
55
39
  )
56
40
  @interactive_option
57
- def create_login_profile(
58
- name: str | None,
59
- api_url: str,
41
+ def create_api_key_profile(
60
42
  project_id: str | None,
61
43
  api_key: str | None,
62
- login_url: str | None,
63
44
  interactive: bool,
64
45
  ) -> None:
65
- """Login to llama cloud control plane as a new profile. May specify name, project ID, and API URL when creating non-interactively"""
46
+ """Authenticate with an API key and create a profile in the current environment."""
66
47
  try:
67
- # If all required args are provided via CLI, skip interactive mode
68
- if name and project_id:
69
- # Use CLI args directly
70
- profile = config_manager.create_profile(name, api_url, project_id, api_key)
71
- rprint(f"[green]Manually created profile '{profile.name}'[/green]")
72
-
73
- # Automatically switch to the new profile
74
- config_manager.set_current_profile(name)
75
- rprint(f"[green]Switched to profile '{name}'[/green]")
76
- return
77
- elif not interactive:
78
- raise click.ClickException(
79
- "No --name or --project-id provided. Run `llamactl auth login --help` for more information."
80
- )
48
+ auth_svc = service.current_auth_service()
81
49
 
82
- # Use interactive creation
83
- profile = create_api_key_profile_form(
84
- api_url=api_url,
85
- project_id=project_id,
86
- api_key_auth_token=api_key,
87
- )
88
- if profile is None:
89
- rprint("[yellow]Cancelled[/yellow]")
50
+ # Non-interactive mode: require both api-key and project-id
51
+ if not interactive:
52
+ if not api_key or not project_id:
53
+ raise click.ClickException(
54
+ "--api-key and --project-id are required in non-interactive mode"
55
+ )
56
+ created = auth_svc.create_profile_from_token(project_id, api_key)
57
+ rprint(
58
+ f"[green]Created API key profile '{created.name}' and set as current[/green]"
59
+ )
90
60
  return
91
61
 
92
- try:
93
- rprint(f"[green]Logged in as '{profile.name}'[/green]")
62
+ # Interactive mode: prompt for token (masked) and validate
63
+ token_value = api_key or _prompt_for_api_key()
64
+ projects = _validate_token_and_list_projects(auth_svc, token_value)
94
65
 
95
- # Automatically switch to the new profile
96
- config_manager.set_current_profile(profile.name)
97
- rprint(f"[green]Switched to profile '{profile.name}'[/green]")
98
- except Exception as e:
99
- rprint(f"[red]Error creating profile: {e}[/red]")
100
- raise click.Abort()
66
+ # Select or enter project ID
67
+ selected_project_id = project_id or _select_or_enter_project(
68
+ projects, auth_svc.env.requires_auth
69
+ )
70
+ if not selected_project_id:
71
+ rprint("[yellow]No project selected[/yellow]")
72
+ return
101
73
 
102
- except ValueError as e:
103
- rprint(f"[red]Error: {e}[/red]")
104
- raise click.Abort()
74
+ # Create and set profile
75
+ created = auth_svc.create_profile_from_token(selected_project_id, token_value)
76
+ rprint(
77
+ f"[green]Created API key profile '{created.name}' and set as current[/green]"
78
+ )
105
79
  except Exception as e:
106
80
  rprint(f"[red]Error: {e}[/red]")
107
81
  raise click.Abort()
108
82
 
109
83
 
110
- @auth.command("token")
111
- @global_options
112
- @click.option(
113
- "--api-url",
114
- help="Specify a custom control plane API URL to log into",
115
- default="https://api.cloud.llamaindex.ai",
116
- )
117
- @click.option(
118
- "--name",
119
- help="Specify a memorable name for the API key login when creating non-interactively",
120
- )
121
- @click.option(
122
- "--project-id",
123
- help="Project ID to use for the login when creating non-interactively",
124
- )
125
- @click.option(
126
- "--api-key",
127
- help="API key to use for the login when creating non-interactively",
128
- )
129
- @interactive_option
130
- def create_api_key_profile(
131
- api_url: str,
132
- name: str | None,
133
- project_id: str | None,
134
- api_key: str | None,
135
- interactive: bool,
136
- ) -> None:
137
- """Authenticate with an API key rather than logging in"""
138
- if not interactive:
139
- if not name or not project_id:
140
- raise click.ClickException(
141
- "No --name or --project-id provided. Run `llamactl auth create-token --help` for more information."
142
- )
143
- profile = config_manager.create_profile(name, api_url, project_id, api_key)
144
- rprint(f"[green]Created API key profile '{profile.name}'[/green]")
145
-
146
- else:
147
- profile = create_api_key_profile_form(
148
- name=name,
149
- api_url=api_url,
150
- project_id=project_id,
151
- api_key_auth_token=api_key,
152
- )
153
- if profile is None:
154
- rprint("[yellow]Cancelled[/yellow]")
155
- return
156
- rprint(f"[green]Created API key profile '{profile.name}'[/green]")
157
- config_manager.set_current_profile(profile.name)
158
-
159
-
160
84
  @auth.command("list")
161
85
  @global_options
162
86
  def list_profiles() -> None:
163
87
  """List all logged in profiles"""
164
88
  try:
165
- profiles = config_manager.list_profiles()
166
- current_name = config_manager.get_current_profile_name()
89
+ auth_svc = service.current_auth_service()
90
+ profiles = auth_svc.list_profiles()
91
+ current = auth_svc.get_current_profile()
167
92
 
168
93
  if not profiles:
169
94
  rprint("[yellow]No profiles found[/yellow]")
170
- rprint("Create one with: [cyan]llamactl profile create[/cyan]")
95
+ rprint("Create one with: [cyan]llamactl auth token[/cyan]")
171
96
  return
172
97
 
173
- table = Table(title="Profiles")
174
- table.add_column("Name", style="cyan")
175
- table.add_column("API URL", style="green")
176
- table.add_column("Active Project", style="yellow")
177
- table.add_column("Current", style="magenta")
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")
178
101
 
179
102
  for profile in profiles:
180
- is_current = "✓" if profile.name == current_name else ""
103
+ text = Text()
104
+ if profile == current:
105
+ text.append("* ", style="magenta")
106
+ else:
107
+ text.append(" ")
108
+ text.append(profile.name)
181
109
  active_project = profile.project_id or "-"
182
- table.add_row(profile.name, profile.api_url, active_project, is_current)
110
+ table.add_row(
111
+ text,
112
+ active_project,
113
+ )
183
114
 
184
115
  console.print(table)
185
116
 
@@ -194,19 +125,15 @@ def list_profiles() -> None:
194
125
  @interactive_option
195
126
  def switch_profile(name: str | None, interactive: bool) -> None:
196
127
  """Switch to a different profile"""
128
+ auth_svc = service.current_auth_service()
197
129
  try:
198
- name = select_profile(name) if interactive else name
199
- if not name:
130
+ selected_auth = _select_profile(auth_svc, name, interactive)
131
+ if not selected_auth:
200
132
  rprint("[yellow]No profile selected[/yellow]")
201
133
  return
202
134
 
203
- profile = config_manager.get_profile(name)
204
- if not profile:
205
- rprint(f"[red]Profile '{name}' not found[/red]")
206
- raise click.Abort()
207
-
208
- config_manager.set_current_profile(name)
209
- rprint(f"[green]Switched to profile '{name}'[/green]")
135
+ auth_svc.set_current_profile(selected_auth.name)
136
+ rprint(f"[green]Switched to profile '{selected_auth.name}'[/green]")
210
137
 
211
138
  except Exception as e:
212
139
  rprint(f"[red]Error: {e}[/red]")
@@ -220,62 +147,16 @@ def switch_profile(name: str | None, interactive: bool) -> None:
220
147
  def delete_profile(name: str | None, interactive: bool) -> None:
221
148
  """Logout from a profile and wipe all associated data"""
222
149
  try:
223
- name = select_profile(name) if interactive else name
224
- if not name:
150
+ auth_svc = service.current_auth_service()
151
+ auth = _select_profile(auth_svc, name, interactive)
152
+ if not auth:
225
153
  rprint("[yellow]No profile selected[/yellow]")
226
154
  return
227
155
 
228
- profile = config_manager.get_profile(name)
229
- if not profile:
230
- rprint(f"[red]Profile '{name}' not found[/red]")
231
- raise click.Abort()
232
-
233
- if config_manager.delete_profile(name):
234
- rprint(f"[green]Logged out from '{name}'[/green]")
156
+ if auth_svc.delete_profile(auth.name):
157
+ rprint(f"[green]Logged out from '{auth.name}'[/green]")
235
158
  else:
236
- rprint(f"[red]Profile '{name}' not found[/red]")
237
-
238
- except Exception as e:
239
- rprint(f"[red]Error: {e}[/red]")
240
- raise click.Abort()
241
-
242
-
243
- @auth.command("edit-token")
244
- @global_options
245
- @click.argument("name", required=False)
246
- def edit_api_key_profile(name: str | None) -> None:
247
- """Edit an API key profile"""
248
- if is_interactive_session():
249
- raise click.ClickException(
250
- "Interactive editing of API key profiles is not supported. You can instead delete and `llamactl auth create-token` to create a new profile."
251
- )
252
- try:
253
- name = select_profile(name)
254
- if not name:
255
- rprint("[yellow]No profile selected[/yellow]")
256
- return
257
-
258
- # Get current profile
259
- maybe_profile = config_manager.get_profile(name)
260
- if not maybe_profile:
261
- rprint(f"[red]Profile '{name}' not found[/red]")
262
- raise click.Abort()
263
- profile = maybe_profile
264
-
265
- # Use the interactive edit menu
266
- updated = edit_api_key_profile_form(profile)
267
- if updated is None:
268
- rprint("[yellow]Cancelled[/yellow]")
269
- return
270
-
271
- try:
272
- current_profile = config_manager.get_current_profile()
273
- if not current_profile or current_profile.name != updated.name:
274
- config_manager.set_current_profile(updated.name)
275
- rprint(f"[green]Updated profile '{profile.name}'[/green]")
276
- except Exception as e:
277
- rprint(f"[red]Error updating profile: {e}[/red]")
278
- raise click.Abort()
159
+ rprint(f"[red]Profile '{auth.name}' not found[/red]")
279
160
 
280
161
  except Exception as e:
281
162
  rprint(f"[red]Error: {e}[/red]")
@@ -290,8 +171,11 @@ def edit_api_key_profile(name: str | None) -> None:
290
171
  def change_project(project_id: str | None, interactive: bool) -> None:
291
172
  """Change the active project for the current profile"""
292
173
  profile = validate_authenticated_profile(interactive)
174
+ if project_id and profile.project_id == project_id:
175
+ return
176
+ auth_svc = service.current_auth_service()
293
177
  if project_id:
294
- config_manager.set_project(profile.name, project_id)
178
+ auth_svc.set_project(profile.name, project_id)
295
179
  rprint(f"[green]Set active project to '{project_id}'[/green]")
296
180
  return
297
181
  if not interactive:
@@ -313,10 +197,18 @@ def change_project(project_id: str | None, interactive: bool) -> None:
313
197
  value=project.project_id,
314
198
  )
315
199
  for project in projects
316
- ],
200
+ ]
201
+ + (
202
+ [questionary.Choice(title="Create new project", value="__CREATE__")]
203
+ if not auth_svc.env.requires_auth
204
+ else []
205
+ ),
317
206
  ).ask()
207
+ if result == "__CREATE__":
208
+ project_id = questionary.text("Enter project ID").ask()
209
+ result = project_id
318
210
  if result:
319
- config_manager.set_project(profile.name, result)
211
+ auth_svc.set_project(profile.name, result)
320
212
  rprint(f"[green]Set active project to '{result}'[/green]")
321
213
  else:
322
214
  rprint("[yellow]No project selected[/yellow]")
@@ -325,58 +217,161 @@ def change_project(project_id: str | None, interactive: bool) -> None:
325
217
  raise click.Abort()
326
218
 
327
219
 
328
- def validate_authenticated_profile(interactive: bool) -> Profile:
329
- """Validate that the user is authenticated"""
220
+ def validate_authenticated_profile(interactive: bool) -> Auth:
221
+ """Validate that the user is authenticated within the current environment.
222
+
223
+ - If there is a current profile, return it.
224
+ - If multiple profiles exist in the current environment, prompt to select in interactive mode.
225
+ - If none exist:
226
+ - If environment requires_auth: run token flow inline.
227
+ - Else: create profile without token after selecting a project.
228
+ """
330
229
 
331
- profile = config_manager.get_current_profile()
332
- if profile:
333
- return profile
334
- elif not interactive:
230
+ auth_svc = service.current_auth_service()
231
+ existing = auth_svc.get_current_profile()
232
+ if existing:
233
+ return existing
234
+
235
+ if not interactive:
335
236
  raise click.ClickException(
336
- "No profile configured. Run `llamactl profile create` to create a profile."
237
+ "No profile configured. Run `llamactl auth token` to create a profile."
337
238
  )
239
+
240
+ # Filter profiles by current environment
241
+ env_profiles = auth_svc.list_profiles()
242
+ current_env = auth_svc.env
243
+
244
+ if len(env_profiles) > 1:
245
+ # Prompt to select
246
+ choice: Auth | None = questionary.select(
247
+ "Select profile",
248
+ choices=[questionary.Choice(title=p.name, value=p) for p in env_profiles],
249
+ ).ask()
250
+ if not choice:
251
+ raise click.ClickException("No profile selected")
252
+ auth_svc.set_current_profile(choice.name)
253
+ return choice
254
+ if len(env_profiles) == 1:
255
+ only = env_profiles[0]
256
+ auth_svc.set_current_profile(only.name)
257
+ return only
258
+
259
+ # No profiles exist for this env
260
+ if current_env.requires_auth:
261
+ # Inline token flow
262
+ created = _token_flow_for_env(auth_svc)
263
+ return created
338
264
  else:
339
- profiles = config_manager.list_profiles()
340
- if len(profiles) > 1:
341
- selected_profile = select_profile()
342
- if not selected_profile:
343
- raise click.ClickException("No profile selected")
344
- config_manager.set_current_profile(selected_profile)
345
- found_profile = config_manager.get_profile(selected_profile)
346
- if found_profile is None:
347
- raise RuntimeError(
348
- f"Unexpected error: Profile '{selected_profile}' not found"
265
+ # No auth required: select project and create a default profile without token
266
+ project_id: str | None = questionary.text("Enter project ID").ask()
267
+ if not project_id:
268
+ raise click.ClickException("No project ID provided")
269
+ created = auth_svc.create_profile_from_token(project_id, None)
270
+ return created
271
+
272
+
273
+ # -----------------------------
274
+ # Helpers for token/profile flow
275
+ # -----------------------------
276
+
277
+
278
+ def _prompt_for_api_key() -> str:
279
+ while True:
280
+ entered = questionary.password("Enter API key token").ask()
281
+ if entered:
282
+ return entered.strip()
283
+ rprint("[yellow]API key is required[/yellow]")
284
+
285
+
286
+ def _validate_token_and_list_projects(
287
+ auth_svc: AuthService, api_key: str
288
+ ) -> list[ProjectSummary]:
289
+ async def _run():
290
+ async with ControlPlaneClient.ctx(auth_svc.env.api_url, api_key) as client:
291
+ return await client.list_projects()
292
+
293
+ try:
294
+ return asyncio.run(_run())
295
+ except ClientError as e:
296
+ if getattr(e, "status_code", None) == 401:
297
+ rprint("[red]Invalid API key. Please try again.[/red]")
298
+ return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
299
+ if getattr(e, "status_code", None) == 403:
300
+ rprint("[red]This environment requires a valid API key.[/red]")
301
+ return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
302
+ raise
303
+ except Exception as e:
304
+ raise click.ClickException(f"Failed to validate API key: {e}")
305
+
306
+
307
+ def _select_or_enter_project(
308
+ projects: list[ProjectSummary], requires_auth: bool
309
+ ) -> str | None:
310
+ if not projects:
311
+ return None
312
+ # select the only authorized project if there is only one
313
+ elif len(projects) == 1 and requires_auth:
314
+ return projects[0].project_id
315
+ else:
316
+ choice = questionary.select(
317
+ "Select a project",
318
+ choices=[
319
+ questionary.Choice(
320
+ title=f"{p.project_name} ({p.deployment_count} deployments)",
321
+ value=p.project_id,
349
322
  )
350
- return found_profile
351
- else:
352
- selected_profile_obj = create_profile_interactive()
353
- if selected_profile_obj is None:
354
- raise click.ClickException("No profile selected")
355
- return selected_profile_obj
356
-
357
-
358
- def create_profile_interactive(
359
- api_url: str = "https://api.cloud.llamaindex.ai",
360
- project_id: str | None = None,
361
- api_key_auth_token: str | None = None,
362
- ) -> Profile | None:
363
- should_continue = questionary.select(
364
- "This action requires you to authenticate with LlamaCloud. Continue?",
365
- choices=[
366
- questionary.Choice(title="Add API Key", value="add_api_key"),
367
- questionary.Choice(title="Cancel", value="cancel"),
368
- ],
369
- ).ask()
370
- if should_continue == "add_api_key":
371
- profile_form = create_api_key_profile_form(
372
- api_url=api_url,
373
- project_id=project_id,
374
- api_key_auth_token=api_key_auth_token,
375
- )
376
- if profile_form is None:
377
- raise click.ClickException("No profile selected")
378
- profile = profile_form.to_profile()
379
- config_manager.set_current_profile(profile.name)
380
- return profile
323
+ for p in projects
324
+ ],
325
+ ).ask()
326
+ return choice
327
+
328
+
329
+ def _token_flow_for_env(auth_service: AuthService) -> Auth:
330
+ token_value = _prompt_for_api_key()
331
+ projects = _validate_token_and_list_projects(auth_service, token_value)
332
+ project_id = _select_or_enter_project(projects, auth_service.env.requires_auth)
333
+ if not project_id:
334
+ raise click.ClickException("No project selected")
335
+ created = auth_service.create_profile_from_token(project_id, token_value)
336
+ return created
381
337
 
382
- return None
338
+
339
+ def _select_profile(
340
+ auth_svc: AuthService, profile_name: str | None, is_interactive: bool
341
+ ) -> Auth | None:
342
+ """
343
+ Select a profile interactively if name not provided.
344
+ Returns the selected profile name or None if cancelled.
345
+
346
+ In non-interactive sessions, returns None if profile_name is not provided.
347
+ """
348
+ if profile_name:
349
+ profile = auth_svc.get_profile(profile_name)
350
+ if profile:
351
+ return profile
352
+
353
+ # Don't attempt interactive selection in non-interactive sessions
354
+ if not is_interactive:
355
+ return None
356
+
357
+ try:
358
+ profiles = auth_svc.list_profiles()
359
+
360
+ if not profiles:
361
+ rprint("[yellow]No profiles found[/yellow]")
362
+ return None
363
+
364
+ choices = []
365
+ current = auth_svc.get_current_profile()
366
+
367
+ for profile in profiles:
368
+ title = f"{profile.name} ({profile.api_url})"
369
+ if profile == current:
370
+ title += " [current]"
371
+ choices.append(questionary.Choice(title=title, value=profile))
372
+
373
+ return questionary.select("Select profile:", choices=choices).ask()
374
+
375
+ except Exception as e:
376
+ rprint(f"[red]Error loading profiles: {e}[/red]")
377
+ return None
@@ -14,6 +14,7 @@ from llama_deploy.cli.commands.auth import validate_authenticated_profile
14
14
  from llama_deploy.core.schema.deployments import DeploymentUpdate
15
15
  from rich import print as rprint
16
16
  from rich.table import Table
17
+ from rich.text import Text
17
18
 
18
19
  from ..app import app, console
19
20
  from ..client import get_project_client
@@ -55,36 +56,23 @@ def list_deployments(interactive: bool) -> None:
55
56
  )
56
57
  return
57
58
 
58
- table = Table(title=f"Deployments for project {client.project_id}")
59
- table.add_column("Name", style="cyan")
60
- table.add_column("ID", style="yellow")
61
- table.add_column("Status", style="green")
62
- table.add_column("Repository", style="blue")
63
- table.add_column("Deployment File", style="magenta")
64
- table.add_column("Git Ref", style="white")
65
- table.add_column("PAT", style="red")
66
- table.add_column("Secrets", style="bright_green")
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("Repository", style="grey46")
67
63
 
68
64
  for deployment in deployments:
69
- name = deployment.name
70
- deployment_id = deployment.id
65
+ name = deployment.id
71
66
  status = deployment.status
72
67
  repo_url = deployment.repo_url
73
- deployment_file_path = deployment.deployment_file_path
74
- git_ref = deployment.git_ref
75
- has_pat = "" if deployment.has_personal_access_token else "-"
76
- secret_names = deployment.secret_names
77
- secrets_display = str(len(secret_names)) if secret_names else "-"
68
+ gh = "https://github.com/"
69
+ if repo_url.startswith(gh):
70
+ repo_url = "gh:" + repo_url.removeprefix(gh)
78
71
 
79
72
  table.add_row(
80
73
  name,
81
- deployment_id,
82
74
  status,
83
75
  repo_url,
84
- deployment_file_path,
85
- git_ref,
86
- has_pat,
87
- secrets_display,
88
76
  )
89
77
 
90
78
  console.print(table)
@@ -114,25 +102,25 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
114
102
 
115
103
  deployment = asyncio.run(client.get_deployment(deployment_id))
116
104
 
117
- table = Table(title=f"Deployment: {deployment.name}")
118
- table.add_column("Property", style="cyan")
119
- table.add_column("Value", style="green")
105
+ table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
106
+ table.add_column("Property", style="grey46", justify="right")
107
+ table.add_column("Value")
120
108
 
121
- table.add_row("ID", deployment.id)
122
- table.add_row("Project ID", deployment.project_id)
123
- table.add_row("Status", deployment.status)
124
- table.add_row("Repository", deployment.repo_url)
125
- table.add_row("Deployment File", deployment.deployment_file_path)
126
- table.add_row("Git Ref", deployment.git_ref)
127
- table.add_row("Has PAT", str(deployment.has_personal_access_token))
109
+ table.add_row("ID", Text(deployment.id))
110
+ table.add_row("Project ID", Text(deployment.project_id))
111
+ table.add_row("Status", Text(deployment.status))
112
+ table.add_row("Repository", Text(deployment.repo_url))
113
+ table.add_row("Deployment File", Text(deployment.deployment_file_path))
114
+ table.add_row("Git Ref", Text(deployment.git_ref or "-"))
128
115
 
129
116
  apiserver_url = deployment.apiserver_url
130
- if apiserver_url:
131
- table.add_row("API Server URL", str(apiserver_url))
117
+ table.add_row(
118
+ "API Server URL",
119
+ Text(str(apiserver_url) if apiserver_url else "-"),
120
+ )
132
121
 
133
- secret_names = deployment.secret_names
134
- if secret_names:
135
- table.add_row("Secrets", ", ".join(secret_names))
122
+ secret_names = deployment.secret_names or []
123
+ table.add_row("Secrets", Text("\n".join(secret_names), style="italic"))
136
124
 
137
125
  console.print(table)
138
126