llamactl 0.3.0a12__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.
- llama_deploy/cli/__init__.py +3 -2
- llama_deploy/cli/app.py +5 -7
- llama_deploy/cli/client.py +31 -24
- llama_deploy/cli/commands/auth.py +377 -0
- llama_deploy/cli/commands/deployment.py +113 -74
- llama_deploy/cli/commands/env.py +206 -0
- llama_deploy/cli/config/_config.py +385 -0
- llama_deploy/cli/config/auth_service.py +68 -0
- llama_deploy/cli/config/env_service.py +64 -0
- llama_deploy/cli/config/schema.py +31 -0
- llama_deploy/cli/interactive_prompts/session_utils.py +37 -0
- llama_deploy/cli/interactive_prompts/utils.py +6 -70
- llama_deploy/cli/options.py +15 -1
- llama_deploy/cli/platform_client.py +52 -0
- llama_deploy/cli/textual/deployment_form.py +11 -10
- llama_deploy/cli/textual/deployment_monitor.py +98 -105
- llama_deploy/cli/textual/git_validation.py +11 -9
- llama_deploy/cli/textual/styles.tcss +21 -5
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a14.dist-info}/METADATA +3 -3
- llamactl-0.3.0a14.dist-info/RECORD +32 -0
- llama_deploy/cli/commands/profile.py +0 -217
- llama_deploy/cli/config.py +0 -173
- llama_deploy/cli/textual/profile_form.py +0 -170
- llamactl-0.3.0a12.dist-info/RECORD +0 -27
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a14.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a14.dist-info}/entry_points.txt +0 -0
|
@@ -6,18 +6,25 @@ A deployment points the control plane at your Git repository and deployment file
|
|
|
6
6
|
git ref, reads the config, and runs your app.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
9
11
|
import click
|
|
12
|
+
import questionary
|
|
13
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
10
14
|
from llama_deploy.core.schema.deployments import DeploymentUpdate
|
|
11
15
|
from rich import print as rprint
|
|
12
16
|
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
13
18
|
|
|
14
19
|
from ..app import app, console
|
|
15
20
|
from ..client import get_project_client
|
|
21
|
+
from ..interactive_prompts.session_utils import (
|
|
22
|
+
is_interactive_session,
|
|
23
|
+
)
|
|
16
24
|
from ..interactive_prompts.utils import (
|
|
17
25
|
confirm_action,
|
|
18
|
-
select_deployment,
|
|
19
26
|
)
|
|
20
|
-
from ..options import global_options
|
|
27
|
+
from ..options import global_options, interactive_option
|
|
21
28
|
from ..textual.deployment_form import create_deployment_form, edit_deployment_form
|
|
22
29
|
from ..textual.deployment_monitor import monitor_deployment_screen
|
|
23
30
|
|
|
@@ -35,11 +42,13 @@ def deployments() -> None:
|
|
|
35
42
|
# Deployments commands
|
|
36
43
|
@deployments.command("list")
|
|
37
44
|
@global_options
|
|
38
|
-
|
|
45
|
+
@interactive_option
|
|
46
|
+
def list_deployments(interactive: bool) -> None:
|
|
39
47
|
"""List deployments for the configured project."""
|
|
48
|
+
validate_authenticated_profile(interactive)
|
|
40
49
|
try:
|
|
41
50
|
client = get_project_client()
|
|
42
|
-
deployments = client.list_deployments()
|
|
51
|
+
deployments = asyncio.run(client.list_deployments())
|
|
43
52
|
|
|
44
53
|
if not deployments:
|
|
45
54
|
rprint(
|
|
@@ -47,36 +56,23 @@ def list_deployments() -> None:
|
|
|
47
56
|
)
|
|
48
57
|
return
|
|
49
58
|
|
|
50
|
-
table = Table(
|
|
51
|
-
table.add_column("Name"
|
|
52
|
-
table.add_column("
|
|
53
|
-
table.add_column("
|
|
54
|
-
table.add_column("Repository", style="blue")
|
|
55
|
-
table.add_column("Deployment File", style="magenta")
|
|
56
|
-
table.add_column("Git Ref", style="white")
|
|
57
|
-
table.add_column("PAT", style="red")
|
|
58
|
-
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")
|
|
59
63
|
|
|
60
64
|
for deployment in deployments:
|
|
61
|
-
name = deployment.
|
|
62
|
-
deployment_id = deployment.id
|
|
65
|
+
name = deployment.id
|
|
63
66
|
status = deployment.status
|
|
64
67
|
repo_url = deployment.repo_url
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
secret_names = deployment.secret_names
|
|
69
|
-
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)
|
|
70
71
|
|
|
71
72
|
table.add_row(
|
|
72
73
|
name,
|
|
73
|
-
deployment_id,
|
|
74
74
|
status,
|
|
75
75
|
repo_url,
|
|
76
|
-
deployment_file_path,
|
|
77
|
-
git_ref,
|
|
78
|
-
has_pat,
|
|
79
|
-
secrets_display,
|
|
80
76
|
)
|
|
81
77
|
|
|
82
78
|
console.print(table)
|
|
@@ -89,13 +85,10 @@ def list_deployments() -> None:
|
|
|
89
85
|
@deployments.command("get")
|
|
90
86
|
@global_options
|
|
91
87
|
@click.argument("deployment_id", required=False)
|
|
92
|
-
@
|
|
93
|
-
|
|
94
|
-
is_flag=True,
|
|
95
|
-
help="Do not open a live monitor screen showing status and streaming logs",
|
|
96
|
-
)
|
|
97
|
-
def get_deployment(deployment_id: str | None, non_interactive: bool) -> None:
|
|
88
|
+
@interactive_option
|
|
89
|
+
def get_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
98
90
|
"""Get details of a specific deployment"""
|
|
91
|
+
validate_authenticated_profile(interactive)
|
|
99
92
|
try:
|
|
100
93
|
client = get_project_client()
|
|
101
94
|
|
|
@@ -103,32 +96,31 @@ def get_deployment(deployment_id: str | None, non_interactive: bool) -> None:
|
|
|
103
96
|
if not deployment_id:
|
|
104
97
|
rprint("[yellow]No deployment selected[/yellow]")
|
|
105
98
|
return
|
|
106
|
-
|
|
107
|
-
if not non_interactive:
|
|
99
|
+
if interactive:
|
|
108
100
|
monitor_deployment_screen(deployment_id)
|
|
109
101
|
return
|
|
110
102
|
|
|
111
|
-
deployment = client.get_deployment(deployment_id)
|
|
103
|
+
deployment = asyncio.run(client.get_deployment(deployment_id))
|
|
112
104
|
|
|
113
|
-
table = Table(
|
|
114
|
-
table.add_column("Property", style="
|
|
115
|
-
table.add_column("Value"
|
|
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")
|
|
116
108
|
|
|
117
|
-
table.add_row("ID", deployment.id)
|
|
118
|
-
table.add_row("Project ID", deployment.project_id)
|
|
119
|
-
table.add_row("Status", deployment.status)
|
|
120
|
-
table.add_row("Repository", deployment.repo_url)
|
|
121
|
-
table.add_row("Deployment File", deployment.deployment_file_path)
|
|
122
|
-
table.add_row("Git Ref", deployment.git_ref)
|
|
123
|
-
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 "-"))
|
|
124
115
|
|
|
125
116
|
apiserver_url = deployment.apiserver_url
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
table.add_row(
|
|
118
|
+
"API Server URL",
|
|
119
|
+
Text(str(apiserver_url) if apiserver_url else "-"),
|
|
120
|
+
)
|
|
128
121
|
|
|
129
|
-
secret_names = deployment.secret_names
|
|
130
|
-
|
|
131
|
-
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"))
|
|
132
124
|
|
|
133
125
|
console.print(table)
|
|
134
126
|
|
|
@@ -139,22 +131,18 @@ def get_deployment(deployment_id: str | None, non_interactive: bool) -> None:
|
|
|
139
131
|
|
|
140
132
|
@deployments.command("create")
|
|
141
133
|
@global_options
|
|
142
|
-
@
|
|
143
|
-
@click.option("--name", help="Deployment name")
|
|
144
|
-
@click.option("--deployment-file-path", help="Path to deployment file")
|
|
145
|
-
@click.option("--git-ref", help="Git reference (branch, tag, or commit)")
|
|
146
|
-
@click.option(
|
|
147
|
-
"--personal-access-token", help="Git Personal Access Token (HTTP Basic Auth)"
|
|
148
|
-
)
|
|
134
|
+
@interactive_option
|
|
149
135
|
def create_deployment(
|
|
150
|
-
|
|
151
|
-
name: str | None,
|
|
152
|
-
deployment_file_path: str | None,
|
|
153
|
-
git_ref: str | None,
|
|
154
|
-
personal_access_token: str | None,
|
|
136
|
+
interactive: bool,
|
|
155
137
|
) -> None:
|
|
156
138
|
"""Interactively create a new deployment"""
|
|
157
139
|
|
|
140
|
+
if not interactive:
|
|
141
|
+
raise click.ClickException(
|
|
142
|
+
"This command requires an interactive session. Run in a terminal or provide required arguments explicitly."
|
|
143
|
+
)
|
|
144
|
+
validate_authenticated_profile(interactive)
|
|
145
|
+
|
|
158
146
|
# Use interactive creation
|
|
159
147
|
deployment_form = create_deployment_form()
|
|
160
148
|
if deployment_form is None:
|
|
@@ -169,23 +157,24 @@ def create_deployment(
|
|
|
169
157
|
@deployments.command("delete")
|
|
170
158
|
@global_options
|
|
171
159
|
@click.argument("deployment_id", required=False)
|
|
172
|
-
@
|
|
173
|
-
def delete_deployment(deployment_id: str | None,
|
|
160
|
+
@interactive_option
|
|
161
|
+
def delete_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
174
162
|
"""Delete a deployment"""
|
|
163
|
+
validate_authenticated_profile(interactive)
|
|
175
164
|
try:
|
|
176
165
|
client = get_project_client()
|
|
177
166
|
|
|
178
|
-
deployment_id = select_deployment(deployment_id)
|
|
167
|
+
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
179
168
|
if not deployment_id:
|
|
180
169
|
rprint("[yellow]No deployment selected[/yellow]")
|
|
181
170
|
return
|
|
182
171
|
|
|
183
|
-
if
|
|
172
|
+
if interactive:
|
|
184
173
|
if not confirm_action(f"Delete deployment '{deployment_id}'?"):
|
|
185
174
|
rprint("[yellow]Cancelled[/yellow]")
|
|
186
175
|
return
|
|
187
176
|
|
|
188
|
-
client.delete_deployment(deployment_id)
|
|
177
|
+
asyncio.run(client.delete_deployment(deployment_id))
|
|
189
178
|
rprint(f"[green]Deleted deployment: {deployment_id}[/green]")
|
|
190
179
|
|
|
191
180
|
except Exception as e:
|
|
@@ -196,18 +185,20 @@ def delete_deployment(deployment_id: str | None, confirm: bool) -> None:
|
|
|
196
185
|
@deployments.command("edit")
|
|
197
186
|
@global_options
|
|
198
187
|
@click.argument("deployment_id", required=False)
|
|
199
|
-
|
|
188
|
+
@interactive_option
|
|
189
|
+
def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
200
190
|
"""Interactively edit a deployment"""
|
|
191
|
+
validate_authenticated_profile(interactive)
|
|
201
192
|
try:
|
|
202
193
|
client = get_project_client()
|
|
203
194
|
|
|
204
|
-
deployment_id = select_deployment(deployment_id)
|
|
195
|
+
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
205
196
|
if not deployment_id:
|
|
206
197
|
rprint("[yellow]No deployment selected[/yellow]")
|
|
207
198
|
return
|
|
208
199
|
|
|
209
200
|
# Get current deployment details
|
|
210
|
-
current_deployment = client.get_deployment(deployment_id)
|
|
201
|
+
current_deployment = asyncio.run(client.get_deployment(deployment_id))
|
|
211
202
|
|
|
212
203
|
# Use the interactive edit form
|
|
213
204
|
updated_deployment = edit_deployment_form(current_deployment)
|
|
@@ -227,8 +218,10 @@ def edit_deployment(deployment_id: str | None) -> None:
|
|
|
227
218
|
@deployments.command("update")
|
|
228
219
|
@global_options
|
|
229
220
|
@click.argument("deployment_id", required=False)
|
|
230
|
-
|
|
221
|
+
@interactive_option
|
|
222
|
+
def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
231
223
|
"""Update the deployment, pulling the latest code from it's branch"""
|
|
224
|
+
validate_authenticated_profile(interactive)
|
|
232
225
|
try:
|
|
233
226
|
client = get_project_client()
|
|
234
227
|
|
|
@@ -238,16 +231,18 @@ def refresh_deployment(deployment_id: str | None) -> None:
|
|
|
238
231
|
return
|
|
239
232
|
|
|
240
233
|
# Get current deployment details to show what we're refreshing
|
|
241
|
-
current_deployment = client.get_deployment(deployment_id)
|
|
234
|
+
current_deployment = asyncio.run(client.get_deployment(deployment_id))
|
|
242
235
|
deployment_name = current_deployment.name
|
|
243
236
|
old_git_sha = current_deployment.git_sha or ""
|
|
244
237
|
|
|
245
238
|
# Create an empty update to force git SHA refresh with spinner
|
|
246
239
|
with console.status(f"Refreshing {deployment_name}..."):
|
|
247
240
|
deployment_update = DeploymentUpdate()
|
|
248
|
-
updated_deployment =
|
|
249
|
-
|
|
250
|
-
|
|
241
|
+
updated_deployment = asyncio.run(
|
|
242
|
+
client.update_deployment(
|
|
243
|
+
deployment_id,
|
|
244
|
+
deployment_update,
|
|
245
|
+
)
|
|
251
246
|
)
|
|
252
247
|
|
|
253
248
|
# Show the git SHA change with short SHAs
|
|
@@ -263,3 +258,47 @@ def refresh_deployment(deployment_id: str | None) -> None:
|
|
|
263
258
|
except Exception as e:
|
|
264
259
|
rprint(f"[red]Error: {e}[/red]")
|
|
265
260
|
raise click.Abort()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def select_deployment(
|
|
264
|
+
deployment_id: str | None = None, interactive: bool = is_interactive_session()
|
|
265
|
+
) -> str | None:
|
|
266
|
+
"""
|
|
267
|
+
Select a deployment interactively if ID not provided.
|
|
268
|
+
Returns the selected deployment ID or None if cancelled.
|
|
269
|
+
|
|
270
|
+
In non-interactive sessions, returns None if deployment_id is not provided.
|
|
271
|
+
"""
|
|
272
|
+
if deployment_id:
|
|
273
|
+
return deployment_id
|
|
274
|
+
|
|
275
|
+
# Don't attempt interactive selection in non-interactive sessions
|
|
276
|
+
if not interactive:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
client = get_project_client()
|
|
281
|
+
deployments = asyncio.run(client.list_deployments())
|
|
282
|
+
|
|
283
|
+
if not deployments:
|
|
284
|
+
rprint(
|
|
285
|
+
f"[yellow]No deployments found for project {client.project_id}[/yellow]"
|
|
286
|
+
)
|
|
287
|
+
return None
|
|
288
|
+
|
|
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
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return questionary.select("Select deployment:", choices=choices).ask()
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
rprint(f"[red]Error loading deployments: {e}[/red]")
|
|
304
|
+
return None
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from importlib import metadata as importlib_metadata
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import questionary
|
|
5
|
+
from llama_deploy.cli.config.schema import Environment
|
|
6
|
+
from packaging import version as packaging_version
|
|
7
|
+
from rich import print as rprint
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from ..app import console
|
|
12
|
+
from ..config.env_service import service
|
|
13
|
+
from ..options import global_options, interactive_option
|
|
14
|
+
from .auth import auth
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@auth.group(
|
|
18
|
+
name="env",
|
|
19
|
+
help="Manage environments (control plane API URLs)",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
@global_options
|
|
23
|
+
def env_group() -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@env_group.command("list")
|
|
28
|
+
@global_options
|
|
29
|
+
def list_environments_cmd() -> None:
|
|
30
|
+
try:
|
|
31
|
+
envs = service.list_environments()
|
|
32
|
+
current_env = service.get_current_environment()
|
|
33
|
+
|
|
34
|
+
if not envs:
|
|
35
|
+
rprint("[yellow]No environments found[/yellow]")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
|
|
39
|
+
table.add_column(" API URL")
|
|
40
|
+
table.add_column("Requires Auth")
|
|
41
|
+
|
|
42
|
+
for env in envs:
|
|
43
|
+
text = Text()
|
|
44
|
+
if env == current_env:
|
|
45
|
+
text.append("* ", style="magenta")
|
|
46
|
+
else:
|
|
47
|
+
text.append(" ")
|
|
48
|
+
text.append(env.api_url)
|
|
49
|
+
table.add_row(
|
|
50
|
+
text, Text("true" if env.requires_auth else "false", style="grey46")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
console.print(table)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
56
|
+
raise click.Abort()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@env_group.command("add")
|
|
60
|
+
@click.argument("api_url", required=False)
|
|
61
|
+
@interactive_option
|
|
62
|
+
@global_options
|
|
63
|
+
def add_environment_cmd(api_url: str | None, interactive: bool) -> None:
|
|
64
|
+
try:
|
|
65
|
+
if not api_url:
|
|
66
|
+
if not interactive:
|
|
67
|
+
raise click.ClickException("API URL is required when not interactive")
|
|
68
|
+
current_env = service.get_current_environment()
|
|
69
|
+
entered = questionary.text(
|
|
70
|
+
"Enter control plane API URL", default=current_env.api_url
|
|
71
|
+
).ask()
|
|
72
|
+
if not entered:
|
|
73
|
+
rprint("[yellow]No environment entered[/yellow]")
|
|
74
|
+
return
|
|
75
|
+
api_url = entered.strip()
|
|
76
|
+
|
|
77
|
+
api_url = api_url.rstrip("/")
|
|
78
|
+
env = service.probe_environment(api_url)
|
|
79
|
+
service.create_or_update_environment(env)
|
|
80
|
+
rprint(
|
|
81
|
+
f"[green]Added environment[/green] {env.api_url} (requires_auth={env.requires_auth}, min_llamactl_version={env.min_llamactl_version or '-'})."
|
|
82
|
+
)
|
|
83
|
+
_maybe_warn_min_version(env.min_llamactl_version)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
rprint(f"[red]Failed to add environment: {e}[/red]")
|
|
86
|
+
raise click.Abort()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@env_group.command("delete")
|
|
90
|
+
@click.argument("api_url", required=False)
|
|
91
|
+
@interactive_option
|
|
92
|
+
@global_options
|
|
93
|
+
def delete_environment_cmd(api_url: str | None, interactive: bool) -> None:
|
|
94
|
+
try:
|
|
95
|
+
if not api_url:
|
|
96
|
+
if not interactive:
|
|
97
|
+
raise click.ClickException("API URL is required when not interactive")
|
|
98
|
+
result = _select_environment(
|
|
99
|
+
service.list_environments(),
|
|
100
|
+
service.get_current_environment(),
|
|
101
|
+
"Select environment to delete",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not result:
|
|
105
|
+
rprint("[yellow]No environment selected[/yellow]")
|
|
106
|
+
return
|
|
107
|
+
api_url = result.api_url
|
|
108
|
+
|
|
109
|
+
api_url = api_url.rstrip("/")
|
|
110
|
+
deleted = service.delete_environment(api_url)
|
|
111
|
+
if not deleted:
|
|
112
|
+
raise click.ClickException(f"Environment '{api_url}' not found")
|
|
113
|
+
rprint(
|
|
114
|
+
f"[green]Deleted environment[/green] {api_url} and all associated profiles"
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
rprint(f"[red]Failed to delete environment: {e}[/red]")
|
|
118
|
+
raise click.Abort()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@env_group.command("switch")
|
|
122
|
+
@click.argument("api_url", required=False)
|
|
123
|
+
@interactive_option
|
|
124
|
+
@global_options
|
|
125
|
+
def switch_environment_cmd(api_url: str | None, interactive: bool) -> None:
|
|
126
|
+
try:
|
|
127
|
+
selected_url = api_url
|
|
128
|
+
|
|
129
|
+
if not selected_url and interactive:
|
|
130
|
+
result = _select_environment(
|
|
131
|
+
service.list_environments(),
|
|
132
|
+
service.get_current_environment(),
|
|
133
|
+
"Select environment",
|
|
134
|
+
)
|
|
135
|
+
if not result:
|
|
136
|
+
rprint("[yellow]No environment selected[/yellow]")
|
|
137
|
+
return
|
|
138
|
+
selected_url = result.api_url
|
|
139
|
+
|
|
140
|
+
if not selected_url:
|
|
141
|
+
if interactive:
|
|
142
|
+
rprint("[yellow]No environment selected[/yellow]")
|
|
143
|
+
return
|
|
144
|
+
raise click.ClickException("API URL is required when not interactive")
|
|
145
|
+
|
|
146
|
+
selected_url = selected_url.rstrip("/")
|
|
147
|
+
|
|
148
|
+
# Ensure environment exists and switch
|
|
149
|
+
env = service.switch_environment(selected_url)
|
|
150
|
+
try:
|
|
151
|
+
env = service.auto_update_env(env)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
rprint(f"[yellow]Failed to resolve environment: {e}[/yellow]")
|
|
154
|
+
return
|
|
155
|
+
service.current_auth_service().select_any_profile()
|
|
156
|
+
rprint(f"[green]Switched to environment[/green] {env.api_url}")
|
|
157
|
+
_maybe_warn_min_version(env.min_llamactl_version)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
rprint(f"[red]Failed to switch environment: {e}[/red]")
|
|
160
|
+
raise click.Abort()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_cli_version() -> str | None:
|
|
164
|
+
try:
|
|
165
|
+
return importlib_metadata.version("llamactl")
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _maybe_warn_min_version(min_required: str | None) -> None:
|
|
171
|
+
if not min_required:
|
|
172
|
+
return
|
|
173
|
+
current = _get_cli_version()
|
|
174
|
+
if not current:
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
if packaging_version.parse(current) < packaging_version.parse(min_required):
|
|
178
|
+
rprint(
|
|
179
|
+
f"[yellow]Warning:[/yellow] This environment requires llamactl >= [bold]{min_required}[/bold], you have [bold]{current}[/bold]."
|
|
180
|
+
)
|
|
181
|
+
except Exception:
|
|
182
|
+
# If packaging is not available or parsing fails, skip strict comparison
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _select_environment(
|
|
187
|
+
envs: list[Environment],
|
|
188
|
+
current_env: Environment,
|
|
189
|
+
message: str = "Select environment",
|
|
190
|
+
) -> Environment | None:
|
|
191
|
+
envs = service.list_environments()
|
|
192
|
+
current_env = service.get_current_environment()
|
|
193
|
+
if not envs:
|
|
194
|
+
raise click.ClickException(
|
|
195
|
+
"No environments found. This is a bug and shouldn't happen."
|
|
196
|
+
)
|
|
197
|
+
return questionary.select(
|
|
198
|
+
message,
|
|
199
|
+
choices=[
|
|
200
|
+
questionary.Choice(
|
|
201
|
+
title=f"{env.api_url} {'(current)' if env.api_url == current_env.api_url else ''}",
|
|
202
|
+
value=env,
|
|
203
|
+
)
|
|
204
|
+
for env in envs
|
|
205
|
+
],
|
|
206
|
+
).ask()
|