vantage-cli 0.1.1__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.
Files changed (46) hide show
  1. vantage_cli/__init__.py +131 -0
  2. vantage_cli/apps/__init__.py +22 -0
  3. vantage_cli/apps/common.py +78 -0
  4. vantage_cli/apps/juju_localhost/__init__.py +17 -0
  5. vantage_cli/apps/juju_localhost/app.py +255 -0
  6. vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
  7. vantage_cli/apps/microk8s/README.md +47 -0
  8. vantage_cli/apps/microk8s/__init__.py +3 -0
  9. vantage_cli/apps/microk8s/app.py +301 -0
  10. vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
  11. vantage_cli/apps/multipass_singlenode/app.py +173 -0
  12. vantage_cli/apps/templates.py +178 -0
  13. vantage_cli/auth.py +429 -0
  14. vantage_cli/cache.py +143 -0
  15. vantage_cli/client.py +84 -0
  16. vantage_cli/command_base.py +63 -0
  17. vantage_cli/commands/__init__.py +1 -0
  18. vantage_cli/commands/clouds/__init__.py +20 -0
  19. vantage_cli/commands/clouds/add.py +81 -0
  20. vantage_cli/commands/clouds/delete.py +61 -0
  21. vantage_cli/commands/clouds/render.py +146 -0
  22. vantage_cli/commands/clouds/update.py +97 -0
  23. vantage_cli/commands/clusters/__init__.py +27 -0
  24. vantage_cli/commands/clusters/create.py +270 -0
  25. vantage_cli/commands/clusters/delete.py +101 -0
  26. vantage_cli/commands/clusters/get.py +30 -0
  27. vantage_cli/commands/clusters/list.py +84 -0
  28. vantage_cli/commands/clusters/render.py +233 -0
  29. vantage_cli/commands/clusters/schema.py +31 -0
  30. vantage_cli/commands/clusters/utils.py +248 -0
  31. vantage_cli/commands/profile/__init__.py +30 -0
  32. vantage_cli/commands/profile/crud.py +529 -0
  33. vantage_cli/commands/profile/render.py +55 -0
  34. vantage_cli/config.py +161 -0
  35. vantage_cli/constants.py +40 -0
  36. vantage_cli/exceptions.py +127 -0
  37. vantage_cli/format.py +39 -0
  38. vantage_cli/gql_client.py +655 -0
  39. vantage_cli/main.py +303 -0
  40. vantage_cli/render.py +56 -0
  41. vantage_cli/schemas.py +48 -0
  42. vantage_cli/time_loop.py +124 -0
  43. vantage_cli-0.1.1.dist-info/METADATA +30 -0
  44. vantage_cli-0.1.1.dist-info/RECORD +46 -0
  45. vantage_cli-0.1.1.dist-info/WHEEL +4 -0
  46. vantage_cli-0.1.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,97 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Update cloud command."""
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from typing_extensions import Annotated
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def update_command(
16
+ ctx: typer.Context,
17
+ cloud_name: Annotated[str, typer.Argument(help="Name of the cloud to update")],
18
+ provider: Annotated[
19
+ Optional[str], typer.Option("--provider", "-p", help="Update cloud provider")
20
+ ] = None,
21
+ region: Annotated[
22
+ Optional[str], typer.Option("--region", "-r", help="Update default region")
23
+ ] = None,
24
+ config_file: Annotated[
25
+ Optional[Path],
26
+ typer.Option(
27
+ "--config-file",
28
+ help="Path to updated configuration file",
29
+ exists=True,
30
+ file_okay=True,
31
+ dir_okay=False,
32
+ readable=True,
33
+ ),
34
+ ] = None,
35
+ credentials_file: Annotated[
36
+ Optional[Path],
37
+ typer.Option(
38
+ "--credentials-file",
39
+ help="Path to updated credentials file",
40
+ exists=True,
41
+ file_okay=True,
42
+ dir_okay=False,
43
+ readable=True,
44
+ ),
45
+ ] = None,
46
+ description: Annotated[
47
+ Optional[str], typer.Option("--description", help="Update cloud description")
48
+ ] = None,
49
+ ):
50
+ """Update an existing cloud configuration."""
51
+ verbose = ctx.obj.get("verbose", False)
52
+ settings = ctx.obj.get("settings")
53
+
54
+ logger.info(f"Updating cloud configuration: {cloud_name}")
55
+
56
+ if verbose:
57
+ logger.debug(f"Provider: {provider}")
58
+ logger.debug(f"Region: {region}")
59
+ logger.debug(f"Config file: {config_file}")
60
+ logger.debug(f"Credentials file: {credentials_file}")
61
+ logger.debug(f"Description: {description}")
62
+ logger.debug(f"Settings: {settings}")
63
+
64
+ # TODO: Check if cloud exists
65
+
66
+ # Check if any updates were provided
67
+ updates = []
68
+ if provider:
69
+ updates.append(f"provider: {provider}")
70
+ if region:
71
+ updates.append(f"region: {region}")
72
+ if config_file:
73
+ updates.append(f"config file: {config_file}")
74
+ if credentials_file:
75
+ updates.append(f"credentials file: {credentials_file}")
76
+ if description:
77
+ updates.append(f"description: {description}")
78
+
79
+ if not updates:
80
+ typer.echo(f"No updates specified for cloud '{cloud_name}'")
81
+ typer.echo("Use --help to see available update options")
82
+ raise typer.Exit(1)
83
+
84
+ typer.echo(f"Updating cloud '{cloud_name}' with:")
85
+ for update in updates:
86
+ typer.echo(f" - {update}")
87
+
88
+ # TODO: Implement actual cloud configuration update logic
89
+ # This would typically:
90
+ # 1. Load existing cloud configuration
91
+ # 2. Update specified fields
92
+ # 3. Validate the updated configuration
93
+ # 4. Save the updated configuration
94
+ # 5. Update any related files or settings
95
+
96
+ logger.info(f"Cloud {cloud_name} updated successfully")
97
+ typer.echo(f"✅ Cloud '{cloud_name}' updated successfully")
@@ -0,0 +1,27 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Cluster management commands for Vantage CLI."""
4
+
5
+ from vantage_cli import AsyncTyper
6
+
7
+ from .create import create_cluster
8
+ from .delete import delete_cluster
9
+ from .get import get_cluster
10
+ from .list import list_clusters
11
+
12
+ # Create the cluster command group
13
+ cluster_app = AsyncTyper(
14
+ name="clusters",
15
+ help="Manage Vantage compute clusters for high-performance computing workloads.",
16
+ context_settings={
17
+ "allow_extra_args": True,
18
+ "allow_interspersed_args": True,
19
+ "ignore_unknown_options": True,
20
+ },
21
+ )
22
+
23
+ # Register subcommands directly
24
+ cluster_app.command("create")(create_cluster)
25
+ cluster_app.command("delete")(delete_cluster)
26
+ cluster_app.command("get")(get_cluster)
27
+ cluster_app.command("list")(list_clusters)
@@ -0,0 +1,270 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Create cluster command for Vantage CLI."""
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import click
9
+ import typer
10
+ from loguru import logger
11
+ from rich.console import Console
12
+ from typing_extensions import Annotated
13
+
14
+ from vantage_cli.config import attach_settings
15
+ from vantage_cli.exceptions import Abort
16
+ from vantage_cli.gql_client import create_async_graphql_client
17
+
18
+ from .render import render_cluster_details
19
+ from .utils import get_app_choices, get_available_apps, get_cloud_choices
20
+
21
+ console = Console()
22
+
23
+
24
+ @attach_settings
25
+ async def create_cluster(
26
+ ctx: typer.Context,
27
+ cluster_name: Annotated[str, typer.Argument(help="Name of the cluster to create")],
28
+ cloud: Annotated[
29
+ str,
30
+ typer.Option(
31
+ "--cloud",
32
+ "-c",
33
+ help="Cloud to use for deployment.",
34
+ case_sensitive=False,
35
+ click_type=click.Choice(get_cloud_choices(), case_sensitive=False),
36
+ ),
37
+ ],
38
+ config_file: Annotated[
39
+ Optional[Path],
40
+ typer.Option(
41
+ "--config-file",
42
+ help="Path to configuration file for cluster creation.",
43
+ exists=True,
44
+ file_okay=True,
45
+ dir_okay=False,
46
+ readable=True,
47
+ ),
48
+ ] = None,
49
+ deploy: Annotated[
50
+ Optional[str],
51
+ typer.Option(
52
+ "--deploy",
53
+ help="Deploy an application after cluster creation.",
54
+ case_sensitive=False,
55
+ click_type=click.Choice(get_app_choices(), case_sensitive=False),
56
+ ),
57
+ ] = None,
58
+ ):
59
+ """Create a new Vantage cluster."""
60
+ # Ensure we have settings configured
61
+ if not ctx.obj or not ctx.obj.settings:
62
+ raise Abort(
63
+ "No settings configured. Please run 'vantage config set' first.",
64
+ subject="Configuration Required",
65
+ log_message="Settings not configured",
66
+ )
67
+
68
+ # Validate cloud provider
69
+ supported_clouds = ctx.obj.settings.supported_clouds
70
+ if cloud not in supported_clouds:
71
+ raise Abort(
72
+ f"Unsupported cloud provider: {cloud}. Supported providers: {', '.join(supported_clouds)}",
73
+ subject="Invalid Cloud Provider",
74
+ log_message=f"Invalid cloud provider: {cloud}",
75
+ )
76
+
77
+ # GraphQL mutation for creating a cluster
78
+ create_mutation = """
79
+ mutation createCluster($createClusterInput: CreateClusterInput!) {
80
+ createCluster(createClusterInput: $createClusterInput) {
81
+ ... on Cluster {
82
+ name
83
+ status
84
+ clientId
85
+ description
86
+ ownerEmail
87
+ provider
88
+ cloudAccountId
89
+ creationParameters
90
+ }
91
+ ... on ClusterNameInUse {
92
+ message
93
+ }
94
+ ... on InvalidInput {
95
+ message
96
+ }
97
+ ... on ClusterCouldNotBeDeployed {
98
+ message
99
+ }
100
+ ... on UnexpectedBehavior {
101
+ message
102
+ }
103
+ }
104
+ }
105
+ """
106
+
107
+ # Map cloud provider to GraphQL enum values
108
+ provider_mapping = {
109
+ "localhost": "on_prem",
110
+ "aws": "aws",
111
+ "gcp": "on_prem", # TODO: Add GCP support to backend
112
+ "azure": "on_prem", # TODO: Add Azure support to backend
113
+ "on-premises": "on_prem",
114
+ }
115
+
116
+ # Build the input variables
117
+ cluster_input: dict[str, Any] = {
118
+ "name": cluster_name,
119
+ "description": f"Cluster {cluster_name} created via CLI",
120
+ "provider": provider_mapping.get(cloud, "on_prem"),
121
+ }
122
+
123
+ # Add provider-specific attributes if needed
124
+ if cloud == "aws":
125
+ # For AWS, we need providerAttributes (camelCase for GraphQL)
126
+ # This is a simplified example - in practice you'd need to collect more details
127
+ cluster_input["providerAttributes"] = {
128
+ "aws": {
129
+ "headNodeInstanceType": "t3.medium", # camelCase for GraphQL
130
+ "keyPair": "default", # This should be configurable
131
+ "cloudAccountId": 1, # This should come from user's cloud account
132
+ "regionName": "us_west_2",
133
+ }
134
+ }
135
+
136
+ # If config file is provided, read and parse it
137
+ if config_file:
138
+ try:
139
+ import json
140
+
141
+ config_data = json.loads(config_file.read_text())
142
+ # Merge config file data with input
143
+ cluster_input.update(config_data)
144
+ except Exception as e:
145
+ raise Abort(
146
+ f"Failed to read configuration file: {e}",
147
+ subject="Configuration File Error",
148
+ log_message=f"Config file error: {e}",
149
+ )
150
+
151
+ variables = {"createClusterInput": cluster_input}
152
+
153
+ try:
154
+ # Create async GraphQL client
155
+ logger.debug(f"CTX OBJ: {ctx.obj}")
156
+ graphql_client = create_async_graphql_client(ctx.obj.settings, ctx.obj.profile)
157
+
158
+ # Execute the mutation
159
+ logger.debug(f"Creating cluster: {cluster_name}")
160
+ console.print(f"[bold blue]Creating cluster '{cluster_name}' on {cloud}...[/bold blue]")
161
+
162
+ response_data = await graphql_client.execute_async(create_mutation, variables)
163
+
164
+ # Handle the response
165
+ create_result = response_data.get("createCluster")
166
+
167
+ if not create_result:
168
+ raise Abort(
169
+ "No response from server",
170
+ subject="Server Error",
171
+ log_message="Empty response from createCluster mutation",
172
+ )
173
+
174
+ # Check for errors in the response
175
+ if "message" in create_result:
176
+ # This is an error response
177
+ error_message = create_result["message"]
178
+ console.print(f"[bold red]Failed to create cluster: {error_message}[/bold red]")
179
+ raise Abort(
180
+ f"Cluster creation failed: {error_message}",
181
+ subject="Cluster Creation Failed",
182
+ log_message=f"GraphQL error: {error_message}",
183
+ )
184
+
185
+ # Success case - cluster was created
186
+ if "name" in create_result:
187
+ console.print(
188
+ f"[bold green]✓ Cluster '{create_result['name']}' created successfully![/bold green]"
189
+ )
190
+ console.print()
191
+
192
+ # Display detailed cluster information
193
+ render_cluster_details(create_result, json_output=False)
194
+
195
+ # Deploy application if --deploy option was provided
196
+ if deploy:
197
+ await deploy_app_to_cluster(ctx, create_result, deploy)
198
+
199
+ else:
200
+ console.print(
201
+ "[bold yellow]Cluster creation initiated but status unclear[/bold yellow]"
202
+ )
203
+
204
+ except Abort:
205
+ # Re-raise Abort exceptions as they contain user-friendly messages
206
+ raise
207
+ except Exception as e:
208
+ logger.error(f"Unexpected error creating cluster: {e}")
209
+ raise Abort(
210
+ "An unexpected error occurred while creating the cluster.",
211
+ subject="Unexpected Error",
212
+ log_message=f"Unexpected error: {e}",
213
+ )
214
+
215
+
216
+ async def deploy_app_to_cluster(ctx: typer.Context, cluster_data: dict, app_name: str):
217
+ """Deploy an application to the newly created cluster."""
218
+ try:
219
+ # Get available apps
220
+ available_apps = get_available_apps()
221
+
222
+ if app_name not in available_apps:
223
+ console.print(f"[bold red]✗ App '{app_name}' not found[/bold red]")
224
+ return
225
+
226
+ console.print(
227
+ f"[bold blue]Deploying app '{app_name}' to cluster '{cluster_data['name']}'...[/bold blue]"
228
+ )
229
+
230
+ # Get the app info
231
+ app_info = available_apps[app_name]
232
+
233
+ # Check if this is a function-based app or class-based app
234
+ if "deploy_function" in app_info:
235
+ # Function-based app
236
+ deploy_function = app_info["deploy_function"]
237
+ await deploy_function(ctx, cluster_data)
238
+ console.print(f"[bold green]✓ App '{app_name}' deployed successfully![/bold green]")
239
+ elif "instance" in app_info:
240
+ # Class-based app
241
+ app_instance = app_info["instance"]
242
+
243
+ # Check if the app has a deploy method
244
+ if hasattr(app_instance, "deploy"):
245
+ # Call the app's deploy method
246
+ await app_instance.deploy(ctx)
247
+ console.print(
248
+ f"[bold green]✓ App '{app_name}' deployed successfully![/bold green]"
249
+ )
250
+ else:
251
+ console.print(
252
+ f"[bold yellow]! App '{app_name}' does not support automatic deployment[/bold yellow]"
253
+ )
254
+ console.print(
255
+ "[dim]You can manually deploy this app using the appropriate commands.[/dim]"
256
+ )
257
+ else:
258
+ console.print(
259
+ f"[bold yellow]! App '{app_name}' does not support automatic deployment[/bold yellow]"
260
+ )
261
+ console.print(
262
+ "[dim]You can manually deploy this app using the appropriate commands.[/dim]"
263
+ )
264
+
265
+ except Exception as e:
266
+ logger.error(f"Failed to deploy app '{app_name}': {e}")
267
+ console.print(f"[bold red]✗ Failed to deploy app '{app_name}': {e}[/bold red]")
268
+ console.print(
269
+ "[dim]The cluster was created successfully, but app deployment failed.[/dim]"
270
+ )
@@ -0,0 +1,101 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Delete cluster command for Vantage CLI."""
4
+
5
+ import typer
6
+ from loguru import logger
7
+ from typing_extensions import Annotated
8
+
9
+ from vantage_cli.command_base import JsonOption, get_effective_json_output
10
+ from vantage_cli.config import attach_settings
11
+ from vantage_cli.exceptions import Abort
12
+ from vantage_cli.gql_client import create_async_graphql_client
13
+
14
+ from .render import render_cluster_deletion_result
15
+
16
+
17
+ @attach_settings
18
+ async def delete_cluster(
19
+ ctx: typer.Context,
20
+ cluster_name: Annotated[str, typer.Argument(help="Name of the cluster to delete")],
21
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation prompt")] = False,
22
+ json_output: JsonOption = False,
23
+ ):
24
+ """Delete a Vantage cluster."""
25
+ # Get effective JSON output setting
26
+ effective_json = get_effective_json_output(ctx, json_output)
27
+
28
+ # Confirmation prompt unless force is used
29
+ if not force and not effective_json:
30
+ from rich.console import Console
31
+ from rich.prompt import Confirm
32
+
33
+ console = Console()
34
+ console.print(
35
+ f"\n[yellow]⚠️ You are about to delete cluster '[bold red]{cluster_name}[/bold red]'[/yellow]"
36
+ )
37
+ console.print("[yellow]This action cannot be undone![/yellow]")
38
+
39
+ if not Confirm.ask("Are you sure you want to proceed?"):
40
+ console.print("[dim]Deletion cancelled.[/dim]")
41
+ return
42
+
43
+ # GraphQL mutation for deleting a cluster
44
+ delete_mutation = """
45
+ mutation deleteCluster($clusterName: String!) {
46
+ deleteCluster(clusterName: $clusterName) {
47
+ ... on ClusterDeleted {
48
+ message
49
+ }
50
+ ... on ClusterNotFound {
51
+ message
52
+ }
53
+ ... on InvalidProviderInput {
54
+ message
55
+ }
56
+ ... on UnexpectedBehavior {
57
+ message
58
+ }
59
+ }
60
+ }
61
+ """
62
+
63
+ try:
64
+ # Create async GraphQL client
65
+ profile = getattr(ctx.obj, "profile", "default")
66
+ graphql_client = create_async_graphql_client(ctx.obj.settings, profile)
67
+
68
+ # Prepare deletion variables
69
+ delete_variables = {"clusterName": cluster_name}
70
+
71
+ # Execute the deletion mutation
72
+ logger.debug(f"Deleting cluster: {cluster_name}")
73
+ delete_response = await graphql_client.execute_async(delete_mutation, delete_variables)
74
+
75
+ # Extract deletion result
76
+ deletion_data = delete_response.get("deleteCluster", {})
77
+
78
+ # Log the response for debugging
79
+ logger.debug(f"Delete response: {deletion_data}")
80
+
81
+ # Determine success - if we get any response it likely succeeded
82
+ # The GraphQL union types make it tricky to detect success vs failure
83
+ success = bool(deletion_data) # If we got a response without error, consider it success
84
+
85
+ # Render deletion result
86
+ render_cluster_deletion_result(
87
+ cluster_name=cluster_name,
88
+ success=success,
89
+ json_output=effective_json,
90
+ )
91
+
92
+ except Abort:
93
+ # Re-raise Abort exceptions as they contain user-friendly messages
94
+ raise
95
+ except Exception as e:
96
+ logger.error(f"Unexpected error deleting cluster '{cluster_name}': {e}")
97
+ raise Abort(
98
+ f"An unexpected error occurred while deleting cluster '{cluster_name}'.",
99
+ subject="Unexpected Error",
100
+ log_message=f"Unexpected error: {e}",
101
+ )
@@ -0,0 +1,30 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Get cluster command for Vantage CLI."""
4
+
5
+ import typer
6
+ from typing_extensions import Annotated
7
+
8
+ from vantage_cli.command_base import JsonOption, get_effective_json_output
9
+ from vantage_cli.commands.clusters.utils import get_cluster_by_name
10
+ from vantage_cli.config import attach_settings
11
+ from vantage_cli.exceptions import Abort
12
+
13
+ from .render import render_cluster_details
14
+
15
+
16
+ @attach_settings
17
+ async def get_cluster(
18
+ ctx: typer.Context,
19
+ cluster_name: Annotated[str, typer.Argument(help="Name of the cluster to get details for")],
20
+ json_output: JsonOption = False,
21
+ ):
22
+ """Get details of a specific Vantage cluster."""
23
+ cluster = await get_cluster_by_name(ctx=ctx, cluster_name=cluster_name)
24
+ if not cluster:
25
+ raise Abort(
26
+ f"No cluster found with name '{cluster_name}'.",
27
+ subject="Cluster Not Found",
28
+ log_message=f"Cluster '{cluster_name}' not found",
29
+ )
30
+ render_cluster_details(cluster, json_output=get_effective_json_output(ctx, json_output))
@@ -0,0 +1,84 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """List clusters command."""
4
+
5
+ import typer
6
+ from loguru import logger
7
+
8
+ from vantage_cli.command_base import JsonOption, get_effective_json_output
9
+ from vantage_cli.config import attach_settings
10
+ from vantage_cli.exceptions import Abort
11
+ from vantage_cli.gql_client import create_async_graphql_client
12
+ from vantage_cli.render import render_quick_start_guide
13
+
14
+ from .render import render_clusters_table
15
+
16
+
17
+ @attach_settings
18
+ async def list_clusters(
19
+ ctx: typer.Context,
20
+ json_output: JsonOption = False,
21
+ ):
22
+ """List all Vantage clusters."""
23
+ # GraphQL query to fetch clusters
24
+ query = """
25
+ query getClusters($first: Int!) {
26
+ clusters(first: $first) {
27
+ edges {
28
+ node {
29
+ name
30
+ status
31
+ clientId
32
+ description
33
+ ownerEmail
34
+ provider
35
+ cloudAccountId
36
+ creationParameters
37
+ }
38
+ }
39
+ total
40
+ }
41
+ }
42
+ """
43
+
44
+ variables = {"first": 100} # Fetch up to 100 clusters
45
+
46
+ try:
47
+ # Create async GraphQL client
48
+ profile = getattr(ctx.obj, "profile", "default")
49
+ graphql_client = create_async_graphql_client(ctx.obj.settings, profile)
50
+
51
+ # Execute the query
52
+ logger.debug("Executing clusters query")
53
+ response_data = await graphql_client.execute_async(query, variables)
54
+
55
+ # Extract cluster data
56
+ clusters_data = response_data.get("clusters", {})
57
+ clusters = [edge["node"] for edge in clusters_data.get("edges", [])]
58
+ total_count = clusters_data.get("total", 0)
59
+
60
+ # Get effective JSON output setting
61
+ effective_json = get_effective_json_output(ctx, json_output)
62
+
63
+ # Render results using Rich table
64
+ render_clusters_table(
65
+ clusters,
66
+ title="Clusters List",
67
+ total_count=total_count,
68
+ json_output=effective_json,
69
+ )
70
+
71
+ # Show quick start guide after listing clusters (only if not JSON output)
72
+ if clusters and not effective_json:
73
+ render_quick_start_guide()
74
+
75
+ except Abort:
76
+ # Re-raise Abort exceptions as they contain user-friendly messages
77
+ raise
78
+ except Exception as e:
79
+ logger.error(f"Unexpected error listing clusters: {e}")
80
+ raise Abort(
81
+ "An unexpected error occurred while listing clusters.",
82
+ subject="Unexpected Error",
83
+ log_message=f"Unexpected error: {e}",
84
+ )