llamactl 0.3.0a4__tar.gz → 0.3.0a5__tar.gz

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.
Files changed (25) hide show
  1. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/PKG-INFO +3 -3
  2. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/pyproject.toml +3 -3
  3. llamactl-0.3.0a5/src/llama_deploy/cli/__init__.py +17 -0
  4. llamactl-0.3.0a5/src/llama_deploy/cli/app.py +71 -0
  5. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/client.py +6 -11
  6. llamactl-0.3.0a5/src/llama_deploy/cli/commands/aliased_group.py +31 -0
  7. llamactl-0.3.0a5/src/llama_deploy/cli/commands/deployment.py +255 -0
  8. llamactl-0.3.0a5/src/llama_deploy/cli/commands/profile.py +217 -0
  9. llamactl-0.3.0a5/src/llama_deploy/cli/commands/serve.py +68 -0
  10. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/config.py +11 -11
  11. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/env.py +3 -2
  12. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/interactive_prompts/utils.py +2 -2
  13. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/options.py +2 -0
  14. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/deployment_form.py +147 -12
  15. llamactl-0.3.0a5/src/llama_deploy/cli/textual/deployment_help.py +53 -0
  16. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/git_validation.py +6 -7
  17. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/github_callback_server.py +1 -1
  18. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/llama_loader.py +1 -0
  19. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/profile_form.py +6 -7
  20. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/secrets_form.py +24 -8
  21. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/textual/styles.tcss +23 -0
  22. llamactl-0.3.0a4/src/llama_deploy/cli/__init__.py +0 -35
  23. llamactl-0.3.0a4/src/llama_deploy/cli/commands.py +0 -577
  24. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/README.md +0 -0
  25. {llamactl-0.3.0a4 → llamactl-0.3.0a5}/src/llama_deploy/cli/debug.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llamactl
3
- Version: 0.3.0a4
3
+ Version: 0.3.0a5
4
4
  Summary: A command-line interface for managing LlamaDeploy projects and deployments
5
5
  Author: Adrian Lyjak
6
6
  Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
7
7
  License: MIT
8
- Requires-Dist: llama-deploy-core>=0.3.0a4,<0.4.0
9
- Requires-Dist: llama-deploy-appserver>=0.3.0a4,<0.4.0
8
+ Requires-Dist: llama-deploy-core>=0.3.0a5,<0.4.0
9
+ Requires-Dist: llama-deploy-appserver>=0.3.0a5,<0.4.0
10
10
  Requires-Dist: httpx>=0.24.0
11
11
  Requires-Dist: rich>=13.0.0
12
12
  Requires-Dist: questionary>=2.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llamactl"
3
- version = "0.3.0a4"
3
+ version = "0.3.0a5"
4
4
  description = "A command-line interface for managing LlamaDeploy projects and deployments"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,8 +9,8 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.12, <4"
11
11
  dependencies = [
12
- "llama-deploy-core>=0.3.0a4,<0.4.0",
13
- "llama-deploy-appserver>=0.3.0a4,<0.4.0",
12
+ "llama-deploy-core>=0.3.0a5,<0.4.0",
13
+ "llama-deploy-appserver>=0.3.0a5,<0.4.0",
14
14
  "httpx>=0.24.0",
15
15
  "rich>=13.0.0",
16
16
  "questionary>=2.0.0",
@@ -0,0 +1,17 @@
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
+
5
+ from .app import app
6
+
7
+
8
+ # Main entry point function (called by the script)
9
+ def main() -> None:
10
+ app()
11
+
12
+
13
+ __all__ = ["app", "deployments", "profile", "serve"]
14
+
15
+
16
+ if __name__ == "__main__":
17
+ app()
@@ -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
@@ -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 ProjectSummary, ProjectsListResponse
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: Optional[str] = None,
73
- project_id: Optional[str] = None,
74
- console: Optional[Console] = None,
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: Optional[str] = None) -> ControlPlaneClient:
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: Optional[str] = None, project_id: Optional[str] = None
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()