llamactl 0.3.0a3__py3-none-any.whl → 0.3.0a5__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 +7 -25
- llama_deploy/cli/app.py +71 -0
- llama_deploy/cli/client.py +6 -11
- llama_deploy/cli/commands/aliased_group.py +31 -0
- llama_deploy/cli/commands/deployment.py +255 -0
- llama_deploy/cli/commands/profile.py +217 -0
- llama_deploy/cli/commands/serve.py +68 -0
- llama_deploy/cli/config.py +11 -11
- llama_deploy/cli/env.py +3 -2
- llama_deploy/cli/interactive_prompts/utils.py +2 -2
- llama_deploy/cli/options.py +2 -0
- llama_deploy/cli/textual/deployment_form.py +147 -12
- llama_deploy/cli/textual/deployment_help.py +53 -0
- llama_deploy/cli/textual/git_validation.py +6 -7
- llama_deploy/cli/textual/github_callback_server.py +1 -1
- llama_deploy/cli/textual/llama_loader.py +1 -0
- llama_deploy/cli/textual/profile_form.py +6 -7
- llama_deploy/cli/textual/secrets_form.py +24 -8
- llama_deploy/cli/textual/styles.tcss +23 -0
- {llamactl-0.3.0a3.dist-info → llamactl-0.3.0a5.dist-info}/METADATA +3 -3
- llamactl-0.3.0a5.dist-info/RECORD +24 -0
- llama_deploy/cli/commands.py +0 -577
- llamactl-0.3.0a3.dist-info/RECORD +0 -19
- {llamactl-0.3.0a3.dist-info → llamactl-0.3.0a5.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a3.dist-info → llamactl-0.3.0a5.dist-info}/entry_points.txt +0 -0
llama_deploy/cli/__init__.py
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
from .commands import
|
|
3
|
-
from .
|
|
1
|
+
from llama_deploy.cli.commands.deployment import deployments
|
|
2
|
+
from llama_deploy.cli.commands.profile import profile
|
|
3
|
+
from llama_deploy.cli.commands.serve import serve
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
# Main CLI application
|
|
7
|
-
@click.group(help="LlamaDeploy CLI - Manage projects and deployments")
|
|
8
|
-
@global_options
|
|
9
|
-
def app():
|
|
10
|
-
"""LlamaDeploy CLI - Manage projects and deployments"""
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Add sub-commands
|
|
15
|
-
app.add_command(profile, name="profile")
|
|
16
|
-
app.add_command(projects, name="project")
|
|
17
|
-
app.add_command(deployments, name="deployment")
|
|
18
|
-
|
|
19
|
-
# Add health check at root level
|
|
20
|
-
app.add_command(health_check, name="health")
|
|
21
|
-
|
|
22
|
-
# Add serve command at root level
|
|
23
|
-
app.add_command(serve, name="serve")
|
|
24
|
-
|
|
25
|
-
# Add version command at root level
|
|
26
|
-
app.add_command(version, name="version")
|
|
5
|
+
from .app import app
|
|
27
6
|
|
|
28
7
|
|
|
29
8
|
# Main entry point function (called by the script)
|
|
@@ -31,5 +10,8 @@ def main() -> None:
|
|
|
31
10
|
app()
|
|
32
11
|
|
|
33
12
|
|
|
13
|
+
__all__ = ["app", "deployments", "profile", "serve"]
|
|
14
|
+
|
|
15
|
+
|
|
34
16
|
if __name__ == "__main__":
|
|
35
17
|
app()
|
llama_deploy/cli/app.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError
|
|
2
|
+
from importlib.metadata import version as pkg_version
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from llama_deploy.cli.client import get_control_plane_client
|
|
6
|
+
from llama_deploy.cli.commands.aliased_group import AliasedGroup
|
|
7
|
+
from llama_deploy.cli.config import config_manager
|
|
8
|
+
from llama_deploy.cli.options import global_options
|
|
9
|
+
from rich import print as rprint
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
console = Console(highlight=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_version(ctx: click.Context, param: click.Option, value: bool) -> None:
|
|
17
|
+
"""Print the version of llama_deploy"""
|
|
18
|
+
if not value or ctx.resilient_parsing:
|
|
19
|
+
return
|
|
20
|
+
try:
|
|
21
|
+
ver = pkg_version("llamactl")
|
|
22
|
+
console.print(Text.assemble("client version: ", (ver, "green")))
|
|
23
|
+
|
|
24
|
+
# If there is an active profile, attempt to query server version
|
|
25
|
+
profile = config_manager.get_current_profile()
|
|
26
|
+
if profile and profile.api_url:
|
|
27
|
+
try:
|
|
28
|
+
cp_client = get_control_plane_client()
|
|
29
|
+
data = cp_client.server_version()
|
|
30
|
+
server_ver = data.get("version")
|
|
31
|
+
console.print(
|
|
32
|
+
Text.assemble(
|
|
33
|
+
"server version: ",
|
|
34
|
+
(
|
|
35
|
+
server_ver or "unknown",
|
|
36
|
+
"bright_yellow" if server_ver is None else "green",
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
console.print(
|
|
42
|
+
Text.assemble(
|
|
43
|
+
"server version: ",
|
|
44
|
+
("unavailable", "bright_yellow"),
|
|
45
|
+
(f" - {e}", "dim"),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
except PackageNotFoundError:
|
|
49
|
+
rprint("[red]Package 'llamactl' not found[/red]")
|
|
50
|
+
raise click.Abort()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
53
|
+
raise click.Abort()
|
|
54
|
+
ctx.exit()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Main CLI application
|
|
58
|
+
@click.group(
|
|
59
|
+
help="Create, develop, and deploy LlamaIndex workflow based apps", cls=AliasedGroup
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--version",
|
|
63
|
+
is_flag=True,
|
|
64
|
+
callback=print_version,
|
|
65
|
+
expose_value=False,
|
|
66
|
+
is_eager=True,
|
|
67
|
+
help="Print client and server versions of LlamaDeploy",
|
|
68
|
+
)
|
|
69
|
+
@global_options
|
|
70
|
+
def app():
|
|
71
|
+
pass
|
llama_deploy/cli/client.py
CHANGED
|
@@ -11,7 +11,7 @@ from llama_deploy.core.schema.git_validation import (
|
|
|
11
11
|
RepositoryValidationRequest,
|
|
12
12
|
RepositoryValidationResponse,
|
|
13
13
|
)
|
|
14
|
-
from llama_deploy.core.schema.projects import
|
|
14
|
+
from llama_deploy.core.schema.projects import ProjectsListResponse, ProjectSummary
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
|
|
17
17
|
from .config import config_manager
|
|
@@ -69,9 +69,9 @@ class ProjectClient(BaseClient):
|
|
|
69
69
|
|
|
70
70
|
def __init__(
|
|
71
71
|
self,
|
|
72
|
-
base_url:
|
|
73
|
-
project_id:
|
|
74
|
-
console:
|
|
72
|
+
base_url: str | None = None,
|
|
73
|
+
project_id: str | None = None,
|
|
74
|
+
console: Console | None = None,
|
|
75
75
|
) -> None:
|
|
76
76
|
# Allow default construction using active profile (for tests and convenience)
|
|
77
77
|
if base_url is None or project_id is None:
|
|
@@ -119,15 +119,10 @@ class ProjectClient(BaseClient):
|
|
|
119
119
|
self,
|
|
120
120
|
deployment_id: str,
|
|
121
121
|
update_data: DeploymentUpdate,
|
|
122
|
-
force_git_sha_update: bool = False,
|
|
123
122
|
) -> DeploymentResponse:
|
|
124
|
-
params = {}
|
|
125
|
-
if force_git_sha_update:
|
|
126
|
-
params["force_git_sha_update"] = True
|
|
127
123
|
response = self.client.patch(
|
|
128
124
|
f"/{self.project_id}/deployments/{deployment_id}",
|
|
129
125
|
json=update_data.model_dump(),
|
|
130
|
-
params=params,
|
|
131
126
|
)
|
|
132
127
|
return DeploymentResponse.model_validate(response.json())
|
|
133
128
|
|
|
@@ -148,7 +143,7 @@ class ProjectClient(BaseClient):
|
|
|
148
143
|
return RepositoryValidationResponse.model_validate(response.json())
|
|
149
144
|
|
|
150
145
|
|
|
151
|
-
def get_control_plane_client(base_url:
|
|
146
|
+
def get_control_plane_client(base_url: str | None = None) -> ControlPlaneClient:
|
|
152
147
|
console = Console()
|
|
153
148
|
profile = config_manager.get_current_profile()
|
|
154
149
|
if not profile and not base_url:
|
|
@@ -163,7 +158,7 @@ def get_control_plane_client(base_url: Optional[str] = None) -> ControlPlaneClie
|
|
|
163
158
|
|
|
164
159
|
|
|
165
160
|
def get_project_client(
|
|
166
|
-
base_url:
|
|
161
|
+
base_url: str | None = None, project_id: str | None = None
|
|
167
162
|
) -> ProjectClient:
|
|
168
163
|
console = Console()
|
|
169
164
|
profile = config_manager.get_current_profile()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Fully lifted from https://click.palletsprojects.com/en/stable/extending-click/"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AliasedGroup(click.Group):
|
|
7
|
+
"""
|
|
8
|
+
Implements a subclass of Group that accepts a prefix for a command.
|
|
9
|
+
If there was a command called push, it would accept pus as an alias (so long as it was unique):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
13
|
+
rv = super().get_command(ctx, cmd_name)
|
|
14
|
+
|
|
15
|
+
if rv is not None:
|
|
16
|
+
return rv
|
|
17
|
+
|
|
18
|
+
matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
|
|
19
|
+
|
|
20
|
+
if not matches:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
if len(matches) == 1:
|
|
24
|
+
return click.Group.get_command(self, ctx, matches[0])
|
|
25
|
+
|
|
26
|
+
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
|
|
27
|
+
|
|
28
|
+
def resolve_command(self, ctx, args):
|
|
29
|
+
# always return the full command name
|
|
30
|
+
_, cmd, args = super().resolve_command(ctx, args)
|
|
31
|
+
return cmd.name, cmd, args
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""CLI commands for managing LlamaDeploy deployments.
|
|
2
|
+
|
|
3
|
+
This command group lets you list, create, edit, refresh, and delete deployments.
|
|
4
|
+
A deployment points the control plane at your Git repository and deployment file
|
|
5
|
+
(e.g., `llama_deploy.yaml`). The control plane pulls your code at the selected
|
|
6
|
+
git ref, reads the config, and runs your app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from llama_deploy.core.schema.deployments import DeploymentUpdate
|
|
11
|
+
from rich import print as rprint
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..app import app, console
|
|
15
|
+
from ..client import get_project_client
|
|
16
|
+
from ..interactive_prompts.utils import (
|
|
17
|
+
confirm_action,
|
|
18
|
+
select_deployment,
|
|
19
|
+
)
|
|
20
|
+
from ..options import global_options
|
|
21
|
+
from ..textual.deployment_form import create_deployment_form, edit_deployment_form
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.group(
|
|
25
|
+
help="Deploy your app to the cloud.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
@global_options
|
|
29
|
+
def deployments() -> None:
|
|
30
|
+
"""Manage deployments"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Deployments commands
|
|
35
|
+
@deployments.command("list")
|
|
36
|
+
@global_options
|
|
37
|
+
def list_deployments() -> None:
|
|
38
|
+
"""List deployments for the configured project."""
|
|
39
|
+
try:
|
|
40
|
+
client = get_project_client()
|
|
41
|
+
deployments = client.list_deployments()
|
|
42
|
+
|
|
43
|
+
if not deployments:
|
|
44
|
+
rprint(
|
|
45
|
+
f"[yellow]No deployments found for project {client.project_id}[/yellow]"
|
|
46
|
+
)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
table = Table(title=f"Deployments for project {client.project_id}")
|
|
50
|
+
table.add_column("Name", style="cyan")
|
|
51
|
+
table.add_column("ID", style="yellow")
|
|
52
|
+
table.add_column("Status", style="green")
|
|
53
|
+
table.add_column("Repository", style="blue")
|
|
54
|
+
table.add_column("Deployment File", style="magenta")
|
|
55
|
+
table.add_column("Git Ref", style="white")
|
|
56
|
+
table.add_column("PAT", style="red")
|
|
57
|
+
table.add_column("Secrets", style="bright_green")
|
|
58
|
+
|
|
59
|
+
for deployment in deployments:
|
|
60
|
+
name = deployment.name
|
|
61
|
+
deployment_id = deployment.id
|
|
62
|
+
status = deployment.status
|
|
63
|
+
repo_url = deployment.repo_url
|
|
64
|
+
deployment_file_path = deployment.deployment_file_path
|
|
65
|
+
git_ref = deployment.git_ref
|
|
66
|
+
has_pat = "✓" if deployment.has_personal_access_token else "-"
|
|
67
|
+
secret_names = deployment.secret_names
|
|
68
|
+
secrets_display = str(len(secret_names)) if secret_names else "-"
|
|
69
|
+
|
|
70
|
+
table.add_row(
|
|
71
|
+
name,
|
|
72
|
+
deployment_id,
|
|
73
|
+
status,
|
|
74
|
+
repo_url,
|
|
75
|
+
deployment_file_path,
|
|
76
|
+
git_ref,
|
|
77
|
+
has_pat,
|
|
78
|
+
secrets_display,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
85
|
+
raise click.Abort()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@deployments.command("get")
|
|
89
|
+
@global_options
|
|
90
|
+
@click.argument("deployment_id", required=False)
|
|
91
|
+
def get_deployment(deployment_id: str | None) -> None:
|
|
92
|
+
"""Get details of a specific deployment"""
|
|
93
|
+
try:
|
|
94
|
+
client = get_project_client()
|
|
95
|
+
|
|
96
|
+
deployment_id = select_deployment(deployment_id)
|
|
97
|
+
if not deployment_id:
|
|
98
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
deployment = client.get_deployment(deployment_id)
|
|
102
|
+
|
|
103
|
+
table = Table(title=f"Deployment: {deployment.name}")
|
|
104
|
+
table.add_column("Property", style="cyan")
|
|
105
|
+
table.add_column("Value", style="green")
|
|
106
|
+
|
|
107
|
+
table.add_row("ID", deployment.id)
|
|
108
|
+
table.add_row("Project ID", deployment.project_id)
|
|
109
|
+
table.add_row("Status", deployment.status)
|
|
110
|
+
table.add_row("Repository", deployment.repo_url)
|
|
111
|
+
table.add_row("Deployment File", deployment.deployment_file_path)
|
|
112
|
+
table.add_row("Git Ref", deployment.git_ref)
|
|
113
|
+
table.add_row("Has PAT", str(deployment.has_personal_access_token))
|
|
114
|
+
|
|
115
|
+
apiserver_url = deployment.apiserver_url
|
|
116
|
+
if apiserver_url:
|
|
117
|
+
table.add_row("API Server URL", str(apiserver_url))
|
|
118
|
+
|
|
119
|
+
secret_names = deployment.secret_names
|
|
120
|
+
if secret_names:
|
|
121
|
+
table.add_row("Secrets", ", ".join(secret_names))
|
|
122
|
+
|
|
123
|
+
console.print(table)
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
127
|
+
raise click.Abort()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@deployments.command("create")
|
|
131
|
+
@global_options
|
|
132
|
+
@click.option("--repo-url", help="HTTP(S) Git Repository URL")
|
|
133
|
+
@click.option("--name", help="Deployment name")
|
|
134
|
+
@click.option("--deployment-file-path", help="Path to deployment file")
|
|
135
|
+
@click.option("--git-ref", help="Git reference (branch, tag, or commit)")
|
|
136
|
+
@click.option(
|
|
137
|
+
"--personal-access-token", help="Git Personal Access Token (HTTP Basic Auth)"
|
|
138
|
+
)
|
|
139
|
+
def create_deployment(
|
|
140
|
+
repo_url: str | None,
|
|
141
|
+
name: str | None,
|
|
142
|
+
deployment_file_path: str | None,
|
|
143
|
+
git_ref: str | None,
|
|
144
|
+
personal_access_token: str | None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Create a new deployment"""
|
|
147
|
+
|
|
148
|
+
# Use interactive creation
|
|
149
|
+
deployment_form = create_deployment_form()
|
|
150
|
+
if deployment_form is None:
|
|
151
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
rprint(
|
|
155
|
+
f"[green]Created deployment: {deployment_form.name} (id: {deployment_form.id})[/green]"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@deployments.command("delete")
|
|
160
|
+
@global_options
|
|
161
|
+
@click.argument("deployment_id", required=False)
|
|
162
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
163
|
+
def delete_deployment(deployment_id: str | None, confirm: bool) -> None:
|
|
164
|
+
"""Delete a deployment"""
|
|
165
|
+
try:
|
|
166
|
+
client = get_project_client()
|
|
167
|
+
|
|
168
|
+
deployment_id = select_deployment(deployment_id)
|
|
169
|
+
if not deployment_id:
|
|
170
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if not confirm:
|
|
174
|
+
if not confirm_action(f"Delete deployment '{deployment_id}'?"):
|
|
175
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
client.delete_deployment(deployment_id)
|
|
179
|
+
rprint(f"[green]Deleted deployment: {deployment_id}[/green]")
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
183
|
+
raise click.Abort()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@deployments.command("edit")
|
|
187
|
+
@global_options
|
|
188
|
+
@click.argument("deployment_id", required=False)
|
|
189
|
+
def edit_deployment(deployment_id: str | None) -> None:
|
|
190
|
+
"""Interactively edit a deployment"""
|
|
191
|
+
try:
|
|
192
|
+
client = get_project_client()
|
|
193
|
+
|
|
194
|
+
deployment_id = select_deployment(deployment_id)
|
|
195
|
+
if not deployment_id:
|
|
196
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Get current deployment details
|
|
200
|
+
current_deployment = client.get_deployment(deployment_id)
|
|
201
|
+
|
|
202
|
+
# Use the interactive edit form
|
|
203
|
+
updated_deployment = edit_deployment_form(current_deployment)
|
|
204
|
+
if updated_deployment is None:
|
|
205
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
rprint(
|
|
209
|
+
f"[green]Successfully updated deployment: {updated_deployment.name}[/green]"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
214
|
+
raise click.Abort()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@deployments.command("refresh")
|
|
218
|
+
@global_options
|
|
219
|
+
@click.argument("deployment_id", required=False)
|
|
220
|
+
def refresh_deployment(deployment_id: str | None) -> None:
|
|
221
|
+
"""Refresh a deployment with the latest code from its git reference"""
|
|
222
|
+
try:
|
|
223
|
+
client = get_project_client()
|
|
224
|
+
|
|
225
|
+
deployment_id = select_deployment(deployment_id)
|
|
226
|
+
if not deployment_id:
|
|
227
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Get current deployment details to show what we're refreshing
|
|
231
|
+
current_deployment = client.get_deployment(deployment_id)
|
|
232
|
+
deployment_name = current_deployment.name
|
|
233
|
+
old_git_sha = current_deployment.git_sha or ""
|
|
234
|
+
|
|
235
|
+
# Create an empty update to force git SHA refresh with spinner
|
|
236
|
+
with console.status(f"Refreshing {deployment_name}..."):
|
|
237
|
+
deployment_update = DeploymentUpdate()
|
|
238
|
+
updated_deployment = client.update_deployment(
|
|
239
|
+
deployment_id,
|
|
240
|
+
deployment_update,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Show the git SHA change with short SHAs
|
|
244
|
+
new_git_sha = updated_deployment.git_sha or ""
|
|
245
|
+
old_short = old_git_sha[:7] if old_git_sha else "none"
|
|
246
|
+
new_short = new_git_sha[:7] if new_git_sha else "none"
|
|
247
|
+
|
|
248
|
+
if old_git_sha == new_git_sha:
|
|
249
|
+
rprint(f"No changes: already at {new_short}")
|
|
250
|
+
else:
|
|
251
|
+
rprint(f"Updated: {old_short} → {new_short}")
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
255
|
+
raise click.Abort()
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from llama_deploy.cli.client import get_control_plane_client
|
|
3
|
+
from rich import print as rprint
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from ..app import app, console
|
|
7
|
+
from ..config import config_manager
|
|
8
|
+
from ..interactive_prompts.utils import (
|
|
9
|
+
select_profile,
|
|
10
|
+
)
|
|
11
|
+
from ..options import global_options
|
|
12
|
+
from ..textual.profile_form import create_profile_form, edit_profile_form
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Create sub-applications for organizing commands
|
|
16
|
+
@app.group(
|
|
17
|
+
help="Login to manage deployments and switch between projects",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
@global_options
|
|
21
|
+
def profile() -> None:
|
|
22
|
+
"""Manage profiles"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Profile commands
|
|
27
|
+
@profile.command("create")
|
|
28
|
+
@global_options
|
|
29
|
+
@click.option("--name", help="Profile name")
|
|
30
|
+
@click.option("--api-url", help="API server URL")
|
|
31
|
+
@click.option("--project-id", help="Default project ID")
|
|
32
|
+
def create_profile(
|
|
33
|
+
name: str | None, api_url: str | None, project_id: str | None
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Create a new profile"""
|
|
36
|
+
try:
|
|
37
|
+
# If all required args are provided via CLI, skip interactive mode
|
|
38
|
+
if name and api_url:
|
|
39
|
+
# Use CLI args directly
|
|
40
|
+
profile = config_manager.create_profile(name, api_url, project_id)
|
|
41
|
+
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
42
|
+
|
|
43
|
+
# Automatically switch to the new profile
|
|
44
|
+
config_manager.set_current_profile(name)
|
|
45
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Use interactive creation
|
|
49
|
+
profile = create_profile_form()
|
|
50
|
+
if profile is None:
|
|
51
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
56
|
+
|
|
57
|
+
# Automatically switch to the new profile
|
|
58
|
+
config_manager.set_current_profile(profile.name)
|
|
59
|
+
rprint(f"[green]Switched to profile '{profile.name}'[/green]")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
rprint(f"[red]Error creating profile: {e}[/red]")
|
|
62
|
+
raise click.Abort()
|
|
63
|
+
|
|
64
|
+
except ValueError as e:
|
|
65
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
66
|
+
raise click.Abort()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
69
|
+
raise click.Abort()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@profile.command("list")
|
|
73
|
+
@global_options
|
|
74
|
+
def list_profiles() -> None:
|
|
75
|
+
"""List all profiles"""
|
|
76
|
+
try:
|
|
77
|
+
profiles = config_manager.list_profiles()
|
|
78
|
+
current_name = config_manager.get_current_profile_name()
|
|
79
|
+
|
|
80
|
+
if not profiles:
|
|
81
|
+
rprint("[yellow]No profiles found[/yellow]")
|
|
82
|
+
rprint("Create one with: [cyan]llamactl profile create[/cyan]")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
table = Table(title="Profiles")
|
|
86
|
+
table.add_column("Name", style="cyan")
|
|
87
|
+
table.add_column("API URL", style="green")
|
|
88
|
+
table.add_column("Active Project", style="yellow")
|
|
89
|
+
table.add_column("Current", style="magenta")
|
|
90
|
+
|
|
91
|
+
for profile in profiles:
|
|
92
|
+
is_current = "✓" if profile.name == current_name else ""
|
|
93
|
+
active_project = profile.active_project_id or "-"
|
|
94
|
+
table.add_row(profile.name, profile.api_url, active_project, is_current)
|
|
95
|
+
|
|
96
|
+
console.print(table)
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
100
|
+
raise click.Abort()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@profile.command("switch")
|
|
104
|
+
@global_options
|
|
105
|
+
@click.argument("name", required=False)
|
|
106
|
+
def switch_profile(name: str | None) -> None:
|
|
107
|
+
"""Switch to a different profile"""
|
|
108
|
+
try:
|
|
109
|
+
name = select_profile(name)
|
|
110
|
+
if not name:
|
|
111
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
profile = config_manager.get_profile(name)
|
|
115
|
+
if not profile:
|
|
116
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
117
|
+
raise click.Abort()
|
|
118
|
+
|
|
119
|
+
config_manager.set_current_profile(name)
|
|
120
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
124
|
+
raise click.Abort()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@profile.command("delete")
|
|
128
|
+
@global_options
|
|
129
|
+
@click.argument("name", required=False)
|
|
130
|
+
def delete_profile(name: str | None) -> None:
|
|
131
|
+
"""Delete a profile"""
|
|
132
|
+
try:
|
|
133
|
+
name = select_profile(name)
|
|
134
|
+
if not name:
|
|
135
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
profile = config_manager.get_profile(name)
|
|
139
|
+
if not profile:
|
|
140
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
141
|
+
raise click.Abort()
|
|
142
|
+
|
|
143
|
+
if config_manager.delete_profile(name):
|
|
144
|
+
rprint(f"[green]Deleted profile '{name}'[/green]")
|
|
145
|
+
else:
|
|
146
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
150
|
+
raise click.Abort()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@profile.command("edit")
|
|
154
|
+
@global_options
|
|
155
|
+
@click.argument("name", required=False)
|
|
156
|
+
def edit_profile(name: str | None) -> None:
|
|
157
|
+
"""Edit a profile"""
|
|
158
|
+
try:
|
|
159
|
+
name = select_profile(name)
|
|
160
|
+
if not name:
|
|
161
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Get current profile
|
|
165
|
+
maybe_profile = config_manager.get_profile(name)
|
|
166
|
+
if not maybe_profile:
|
|
167
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
168
|
+
raise click.Abort()
|
|
169
|
+
profile = maybe_profile
|
|
170
|
+
|
|
171
|
+
# Use the interactive edit menu
|
|
172
|
+
updated = edit_profile_form(profile)
|
|
173
|
+
if updated is None:
|
|
174
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
current_profile = config_manager.get_current_profile()
|
|
179
|
+
if not current_profile or current_profile.name != updated.name:
|
|
180
|
+
config_manager.set_current_profile(updated.name)
|
|
181
|
+
rprint(f"[green]Updated profile '{profile.name}'[/green]")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
rprint(f"[red]Error updating profile: {e}[/red]")
|
|
184
|
+
raise click.Abort()
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
188
|
+
raise click.Abort()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Projects commands
|
|
192
|
+
@profile.command("list-projects")
|
|
193
|
+
@global_options
|
|
194
|
+
def list_projects() -> None:
|
|
195
|
+
"""List all projects with deployment counts"""
|
|
196
|
+
try:
|
|
197
|
+
client = get_control_plane_client()
|
|
198
|
+
projects = client.list_projects()
|
|
199
|
+
|
|
200
|
+
if not projects:
|
|
201
|
+
rprint("[yellow]No projects found[/yellow]")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
table = Table(title="Projects")
|
|
205
|
+
table.add_column("Project ID", style="cyan")
|
|
206
|
+
table.add_column("Deployments", style="green")
|
|
207
|
+
|
|
208
|
+
for project in projects:
|
|
209
|
+
project_id = project.project_id
|
|
210
|
+
deployment_count = project.deployment_count
|
|
211
|
+
table.add_row(project_id, str(deployment_count))
|
|
212
|
+
|
|
213
|
+
console.print(table)
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
217
|
+
raise click.Abort()
|