qbraid-cli 0.8.5a1__py3-none-any.whl → 0.12.0a0__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.
- qbraid_cli/_version.py +16 -14
- qbraid_cli/account/__init__.py +11 -0
- qbraid_cli/account/app.py +67 -0
- qbraid_cli/admin/app.py +21 -13
- qbraid_cli/admin/headers.py +132 -23
- qbraid_cli/admin/validation.py +1 -8
- qbraid_cli/chat/__init__.py +11 -0
- qbraid_cli/chat/app.py +76 -0
- qbraid_cli/configure/actions.py +21 -2
- qbraid_cli/configure/app.py +147 -2
- qbraid_cli/configure/claude_config.py +215 -0
- qbraid_cli/devices/app.py +27 -4
- qbraid_cli/envs/activate.py +38 -8
- qbraid_cli/envs/app.py +716 -89
- qbraid_cli/envs/create.py +3 -2
- qbraid_cli/files/__init__.py +11 -0
- qbraid_cli/files/app.py +139 -0
- qbraid_cli/handlers.py +35 -5
- qbraid_cli/jobs/app.py +33 -13
- qbraid_cli/jobs/toggle_braket.py +2 -13
- qbraid_cli/jobs/validation.py +1 -0
- qbraid_cli/kernels/app.py +4 -3
- qbraid_cli/main.py +57 -13
- qbraid_cli/mcp/__init__.py +10 -0
- qbraid_cli/mcp/app.py +126 -0
- qbraid_cli/mcp/serve.py +321 -0
- qbraid_cli/pip/app.py +2 -2
- qbraid_cli/pip/hooks.py +1 -0
- {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/METADATA +37 -14
- qbraid_cli-0.12.0a0.dist-info/RECORD +46 -0
- {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/WHEEL +1 -1
- {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info/licenses}/LICENSE +6 -4
- qbraid_cli/admin/buildlogs.py +0 -114
- qbraid_cli/credits/__init__.py +0 -11
- qbraid_cli/credits/app.py +0 -35
- qbraid_cli-0.8.5a1.dist-info/RECORD +0 -39
- {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/entry_points.txt +0 -0
- {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/top_level.txt +0 -0
qbraid_cli/envs/app.py
CHANGED
|
@@ -6,50 +6,86 @@ Module defining commands in the 'qbraid envs' namespace.
|
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import shutil
|
|
10
9
|
import subprocess
|
|
11
10
|
import sys
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import TYPE_CHECKING, Optional
|
|
14
13
|
|
|
15
14
|
import typer
|
|
15
|
+
from qbraid_core.services.environments.schema import EnvironmentConfig
|
|
16
|
+
from qbraid_core.services.environments.exceptions import EnvironmentValidationError
|
|
16
17
|
from rich.console import Console
|
|
18
|
+
from rich.progress import (
|
|
19
|
+
BarColumn,
|
|
20
|
+
DownloadColumn,
|
|
21
|
+
Progress,
|
|
22
|
+
SpinnerColumn,
|
|
23
|
+
TextColumn,
|
|
24
|
+
TimeElapsedColumn,
|
|
25
|
+
TransferSpeedColumn,
|
|
26
|
+
)
|
|
17
27
|
|
|
18
28
|
from qbraid_cli.envs.create import create_qbraid_env_assets, create_venv
|
|
19
29
|
from qbraid_cli.envs.data_handling import get_envs_data as installed_envs_data
|
|
20
30
|
from qbraid_cli.envs.data_handling import validate_env_name
|
|
21
|
-
from qbraid_cli.handlers import QbraidException, run_progress_task
|
|
31
|
+
from qbraid_cli.handlers import QbraidException, handle_error, run_progress_task
|
|
22
32
|
|
|
23
33
|
if TYPE_CHECKING:
|
|
24
34
|
from qbraid_core.services.environments.client import EnvironmentManagerClient as EMC
|
|
25
35
|
|
|
26
|
-
envs_app = typer.Typer(help="Manage qBraid environments.")
|
|
36
|
+
envs_app = typer.Typer(help="Manage qBraid environments.", no_args_is_help=True)
|
|
27
37
|
|
|
28
38
|
|
|
29
39
|
@envs_app.command(name="create")
|
|
30
40
|
def envs_create( # pylint: disable=too-many-statements
|
|
31
|
-
name: str = typer.Option(
|
|
32
|
-
..., "--name", "-n", help="Name of the environment to create", callback=validate_env_name
|
|
33
|
-
),
|
|
41
|
+
name: str = typer.Option(None, "--name", "-n", help="Name of the environment to create"),
|
|
34
42
|
description: Optional[str] = typer.Option(
|
|
35
43
|
None, "--description", "-d", help="Short description of the environment"
|
|
36
44
|
),
|
|
45
|
+
file_path: str = typer.Option(
|
|
46
|
+
None, "--file", "-f", help="Path to a .yml file containing the environment details"
|
|
47
|
+
),
|
|
37
48
|
auto_confirm: bool = typer.Option(
|
|
38
49
|
False, "--yes", "-y", help="Automatically answer 'yes' to all prompts"
|
|
39
50
|
),
|
|
40
51
|
) -> None:
|
|
41
52
|
"""Create a new qBraid environment."""
|
|
42
53
|
env_description = description or ""
|
|
54
|
+
if name:
|
|
55
|
+
if not validate_env_name(name):
|
|
56
|
+
handle_error(
|
|
57
|
+
error_type="ValueError",
|
|
58
|
+
include_traceback=False,
|
|
59
|
+
message=f"Invalid environment name '{name}'. ",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
env_details_in_cli = name is not None and env_description != ""
|
|
63
|
+
env_config = None
|
|
64
|
+
if env_details_in_cli and file_path:
|
|
65
|
+
handle_error(
|
|
66
|
+
error_type="ArgumentConflictError",
|
|
67
|
+
include_traceback=False,
|
|
68
|
+
message="Cannot use --file with --name or --description while creating an environment",
|
|
69
|
+
)
|
|
70
|
+
elif not env_details_in_cli and not file_path:
|
|
71
|
+
handle_error(
|
|
72
|
+
error_type="MalformedCommandError",
|
|
73
|
+
include_traceback=False,
|
|
74
|
+
message="Must provide either --name and --description or --file "
|
|
75
|
+
"while creating an environment",
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
try:
|
|
79
|
+
if file_path:
|
|
80
|
+
env_config: EnvironmentConfig = EnvironmentConfig.from_yaml(file_path)
|
|
81
|
+
except ValueError as err:
|
|
82
|
+
handle_error(error_type="YamlValidationError", message=str(err))
|
|
43
83
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
47
|
-
|
|
48
|
-
client = EnvironmentManagerClient()
|
|
49
|
-
return client.create_environment(*args, **kwargs), client
|
|
84
|
+
if not name:
|
|
85
|
+
name = env_config.name
|
|
50
86
|
|
|
51
87
|
def gather_local_data() -> tuple[Path, str]:
|
|
52
|
-
"""Gather environment data
|
|
88
|
+
"""Gather local environment data for creation."""
|
|
53
89
|
from qbraid_core.services.environments import get_default_envs_paths
|
|
54
90
|
|
|
55
91
|
env_path = get_default_envs_paths()[0]
|
|
@@ -65,69 +101,71 @@ def envs_create( # pylint: disable=too-many-statements
|
|
|
65
101
|
|
|
66
102
|
return env_path, python_version
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
error_message="Failed to create qBraid environment",
|
|
74
|
-
)
|
|
104
|
+
if not env_config:
|
|
105
|
+
env_config = EnvironmentConfig(
|
|
106
|
+
name=name,
|
|
107
|
+
description=env_description,
|
|
108
|
+
)
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
# NOTE: create_environment API call will be removed from CLI
|
|
111
|
+
# For now, we generate env_id locally and create environment without API
|
|
112
|
+
from qbraid_core.services.environments.registry import generate_env_id
|
|
113
|
+
|
|
114
|
+
local_data_out: tuple[Path, str] = run_progress_task(
|
|
77
115
|
gather_local_data,
|
|
78
116
|
description="Solving environment...",
|
|
79
117
|
error_message="Failed to create qBraid environment",
|
|
80
118
|
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
|
|
120
|
+
env_path, python_version = local_data_out
|
|
121
|
+
|
|
122
|
+
env_config.python_version = python_version
|
|
123
|
+
|
|
124
|
+
# Generate env_id for local environment (slug will be set when published)
|
|
125
|
+
env_id = generate_env_id()
|
|
126
|
+
env_id_path = env_path / env_id
|
|
89
127
|
description = "None" if description == "" else description
|
|
90
128
|
|
|
91
|
-
typer.echo("
|
|
92
|
-
typer.echo(f" name: {
|
|
93
|
-
typer.echo(f" description: {description}")
|
|
94
|
-
typer.echo(f" tags: {tags}")
|
|
95
|
-
typer.echo(f"
|
|
96
|
-
typer.echo(f" shellPrompt: {
|
|
97
|
-
typer.echo(f" kernelName: {kernel_name}")
|
|
129
|
+
typer.echo("## qBraid Metadata ##\n")
|
|
130
|
+
typer.echo(f" name: {env_config.name}")
|
|
131
|
+
typer.echo(f" description: {env_config.description}")
|
|
132
|
+
typer.echo(f" tags: {env_config.tags}")
|
|
133
|
+
typer.echo(f" env_id: {env_id}")
|
|
134
|
+
typer.echo(f" shellPrompt: {env_config.shell_prompt}")
|
|
135
|
+
typer.echo(f" kernelName: {env_config.kernel_name}")
|
|
98
136
|
|
|
99
137
|
typer.echo("\n\n## Environment Plan ##\n")
|
|
100
|
-
typer.echo(f" location: {
|
|
138
|
+
typer.echo(f" location: {env_id_path}")
|
|
101
139
|
typer.echo(f" version: {python_version}\n")
|
|
102
140
|
|
|
103
141
|
user_confirmation = auto_confirm or typer.confirm("Proceed", default=True)
|
|
104
142
|
typer.echo("")
|
|
105
143
|
if not user_confirmation:
|
|
106
|
-
emc.delete_environment(slug)
|
|
107
144
|
typer.echo("qBraidSystemExit: Exiting.")
|
|
108
145
|
raise typer.Exit()
|
|
109
146
|
|
|
110
147
|
run_progress_task(
|
|
111
148
|
create_qbraid_env_assets,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
slug_path,
|
|
149
|
+
env_id,
|
|
150
|
+
env_id_path,
|
|
151
|
+
env_config,
|
|
116
152
|
description="Generating qBraid assets...",
|
|
117
153
|
error_message="Failed to create qBraid environment",
|
|
118
154
|
)
|
|
119
155
|
|
|
120
156
|
run_progress_task(
|
|
121
157
|
create_venv,
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
env_id_path,
|
|
159
|
+
env_config.shell_prompt,
|
|
160
|
+
None, # python_exe (use default)
|
|
161
|
+
env_id, # env_id for registry package tracking
|
|
124
162
|
description="Creating virtual environment...",
|
|
125
163
|
error_message="Failed to create qBraid environment",
|
|
126
164
|
)
|
|
127
165
|
|
|
128
166
|
console = Console()
|
|
129
167
|
console.print(
|
|
130
|
-
f"
|
|
168
|
+
f"[bold green]Successfully created qBraid environment: "
|
|
131
169
|
f"[/bold green][bold magenta]{name}[/bold magenta]\n"
|
|
132
170
|
)
|
|
133
171
|
typer.echo("# To activate this environment, use")
|
|
@@ -147,30 +185,43 @@ def envs_remove(
|
|
|
147
185
|
),
|
|
148
186
|
) -> None:
|
|
149
187
|
"""Delete a qBraid environment."""
|
|
188
|
+
import asyncio
|
|
150
189
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
190
|
+
def uninstall_environment(slug: str) -> dict:
|
|
191
|
+
"""Uninstall a qBraid environment using async method."""
|
|
153
192
|
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
154
193
|
|
|
155
194
|
emc = EnvironmentManagerClient()
|
|
156
|
-
|
|
195
|
+
|
|
196
|
+
# Run async uninstall method
|
|
197
|
+
result = asyncio.run(
|
|
198
|
+
emc.uninstall_environment_local(slug, delete_metadata=True, force=auto_confirm)
|
|
199
|
+
)
|
|
200
|
+
return result
|
|
157
201
|
|
|
158
202
|
def gather_local_data(env_name: str) -> tuple[Path, str]:
|
|
159
|
-
"""Get environment path and
|
|
203
|
+
"""Get environment path and env_id from name or env_id."""
|
|
160
204
|
installed, aliases = installed_envs_data()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
# Try to find by name first, then by env_id
|
|
206
|
+
if env_name in aliases:
|
|
207
|
+
env_id = aliases[env_name]
|
|
208
|
+
path = installed[env_id]
|
|
209
|
+
return path, env_id
|
|
210
|
+
elif env_name in installed:
|
|
211
|
+
# Direct env_id lookup
|
|
212
|
+
path = installed[env_name]
|
|
213
|
+
return path, env_name
|
|
214
|
+
|
|
215
|
+
raise QbraidException(
|
|
216
|
+
f"Environment '{name}' not found. "
|
|
217
|
+
"Use name (if unique) or env_id to reference the environment."
|
|
218
|
+
)
|
|
168
219
|
|
|
169
|
-
|
|
220
|
+
env_path, env_id = gather_local_data(name)
|
|
170
221
|
|
|
171
222
|
confirmation_message = (
|
|
172
223
|
f"⚠️ Warning: You are about to delete the environment '{name}' "
|
|
173
|
-
f"located at '{
|
|
224
|
+
f"located at '{env_path}'.\n"
|
|
174
225
|
"This will remove all local packages and permanently delete all "
|
|
175
226
|
"of its associated qBraid environment metadata.\n"
|
|
176
227
|
"This operation CANNOT be undone.\n\n"
|
|
@@ -179,27 +230,25 @@ def envs_remove(
|
|
|
179
230
|
|
|
180
231
|
if auto_confirm or typer.confirm(confirmation_message, abort=True):
|
|
181
232
|
typer.echo("")
|
|
182
|
-
run_progress_task(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
description="Deleting
|
|
233
|
+
result = run_progress_task(
|
|
234
|
+
uninstall_environment,
|
|
235
|
+
env_id, # Can be name or env_id - method handles both
|
|
236
|
+
description="Deleting environment...",
|
|
186
237
|
error_message="Failed to delete qBraid environment",
|
|
187
238
|
)
|
|
188
239
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
error_message="Failed to delete qBraid environment",
|
|
194
|
-
)
|
|
195
|
-
typer.echo(f"\nEnvironment '{name}' successfully removed.")
|
|
240
|
+
if result.get('deleted_metadata'):
|
|
241
|
+
typer.echo(f"✅ Environment '{name}' successfully removed (local files and metadata).")
|
|
242
|
+
else:
|
|
243
|
+
typer.echo(f"✅ Environment '{name}' successfully removed (local files only).")
|
|
196
244
|
|
|
197
245
|
|
|
198
246
|
@envs_app.command(name="list")
|
|
199
247
|
def envs_list():
|
|
200
248
|
"""List installed qBraid environments."""
|
|
201
|
-
|
|
202
|
-
|
|
249
|
+
from qbraid_core.services.environments.registry import EnvironmentRegistryManager
|
|
250
|
+
|
|
251
|
+
installed, aliases = installed_envs_data(use_registry=True)
|
|
203
252
|
console = Console()
|
|
204
253
|
|
|
205
254
|
if len(installed) == 0:
|
|
@@ -210,28 +259,55 @@ def envs_list():
|
|
|
210
259
|
)
|
|
211
260
|
return
|
|
212
261
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
262
|
+
# Get registry to access name and env_id
|
|
263
|
+
registry_mgr = EnvironmentRegistryManager()
|
|
264
|
+
|
|
265
|
+
# Build list of (name, env_id, path) tuples
|
|
266
|
+
env_list = []
|
|
267
|
+
for env_id, path in installed.items():
|
|
268
|
+
try:
|
|
269
|
+
entry = registry_mgr.get_environment(env_id)
|
|
270
|
+
env_list.append((entry.name, env_id, path))
|
|
271
|
+
except Exception:
|
|
272
|
+
# Fallback if registry lookup fails
|
|
273
|
+
# Try to get name from aliases
|
|
274
|
+
name = None
|
|
275
|
+
for alias, eid in aliases.items():
|
|
276
|
+
if eid == env_id and alias != env_id:
|
|
277
|
+
name = alias
|
|
278
|
+
break
|
|
279
|
+
if not name:
|
|
280
|
+
name = env_id
|
|
281
|
+
env_list.append((name, env_id, path))
|
|
282
|
+
|
|
283
|
+
# Sort: default first, then by name
|
|
284
|
+
sorted_env_list = sorted(
|
|
285
|
+
env_list,
|
|
286
|
+
key=lambda x: (x[0] != "default", str(x[2]).startswith(str(Path.home())), x[0]),
|
|
217
287
|
)
|
|
218
288
|
|
|
219
289
|
current_env_path = Path(sys.executable).parent.parent.parent
|
|
220
290
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
291
|
+
# Calculate column widths
|
|
292
|
+
max_name_length = max(len(str(name)) for name, _, _ in sorted_env_list) if sorted_env_list else 0
|
|
293
|
+
max_env_id_length = max(len(str(env_id)) for _, env_id, _ in sorted_env_list) if sorted_env_list else 0
|
|
294
|
+
name_width = max(max_name_length, 4) # At least "Name"
|
|
295
|
+
env_id_width = max(max_env_id_length, 6) # At least "Env ID"
|
|
226
296
|
|
|
227
297
|
output_lines = []
|
|
228
298
|
output_lines.append("# qbraid environments:")
|
|
229
299
|
output_lines.append("#")
|
|
230
300
|
output_lines.append("")
|
|
301
|
+
|
|
302
|
+
# Header
|
|
303
|
+
header = f"{'Name'.ljust(name_width + 2)}{'Env ID'.ljust(env_id_width + 2)}Path"
|
|
304
|
+
output_lines.append(header)
|
|
305
|
+
output_lines.append("")
|
|
231
306
|
|
|
232
|
-
|
|
307
|
+
# Data rows
|
|
308
|
+
for name, env_id, path in sorted_env_list:
|
|
233
309
|
mark = "*" if path == current_env_path else " "
|
|
234
|
-
line = f"{
|
|
310
|
+
line = f"{name.ljust(name_width + 2)}{env_id.ljust(env_id_width + 2)}{mark} {path}"
|
|
235
311
|
output_lines.append(line)
|
|
236
312
|
|
|
237
313
|
final_output = "\n".join(output_lines)
|
|
@@ -241,24 +317,575 @@ def envs_list():
|
|
|
241
317
|
|
|
242
318
|
@envs_app.command(name="activate")
|
|
243
319
|
def envs_activate(
|
|
244
|
-
name: str = typer.Argument(
|
|
320
|
+
name: str = typer.Argument(
|
|
321
|
+
..., help="Name of the environment. Values from 'qbraid envs list'."
|
|
322
|
+
),
|
|
245
323
|
):
|
|
246
324
|
"""Activate qBraid environment.
|
|
247
325
|
|
|
248
326
|
NOTE: Currently only works on qBraid Lab platform, and select few other OS types.
|
|
249
327
|
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
328
|
+
from qbraid_core.services.environments.registry import EnvironmentRegistryManager
|
|
329
|
+
|
|
330
|
+
# Get environment details from registry by looking up alias
|
|
331
|
+
installed, aliases = installed_envs_data(use_registry=True)
|
|
332
|
+
# Find the env_id from the alias (name or env_id)
|
|
333
|
+
env_id = aliases.get(name)
|
|
334
|
+
|
|
335
|
+
if not env_id:
|
|
256
336
|
raise typer.BadParameter(f"Environment '{name}' not found.")
|
|
257
337
|
|
|
338
|
+
# Get the environment entry
|
|
339
|
+
registry_mgr = EnvironmentRegistryManager()
|
|
340
|
+
entry = registry_mgr.get_environment(env_id)
|
|
341
|
+
env_path = Path(entry.path)
|
|
342
|
+
|
|
343
|
+
# The venv is always at pyenv/ subdirectory
|
|
344
|
+
# (shell_prompt is for the PS1 display name, not the directory)
|
|
345
|
+
venv_path = env_path / "pyenv"
|
|
346
|
+
|
|
258
347
|
from .activate import activate_pyvenv
|
|
259
348
|
|
|
260
349
|
activate_pyvenv(venv_path)
|
|
261
350
|
|
|
262
351
|
|
|
352
|
+
@envs_app.command(name="available")
|
|
353
|
+
def envs_available():
|
|
354
|
+
"""List available pre-built environments for installation."""
|
|
355
|
+
|
|
356
|
+
def get_available_environments():
|
|
357
|
+
"""Get available environments from the API."""
|
|
358
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
359
|
+
|
|
360
|
+
emc = EnvironmentManagerClient()
|
|
361
|
+
return emc.get_available_environments()
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
result = run_progress_task(
|
|
365
|
+
get_available_environments,
|
|
366
|
+
description="Fetching available environments...",
|
|
367
|
+
error_message="Failed to fetch available environments",
|
|
368
|
+
)
|
|
369
|
+
except QbraidException:
|
|
370
|
+
# If API fails, show a helpful message
|
|
371
|
+
console = Console()
|
|
372
|
+
console.print(
|
|
373
|
+
"[yellow]Unable to fetch available environments from qBraid service.[/yellow]"
|
|
374
|
+
)
|
|
375
|
+
console.print("This feature requires:")
|
|
376
|
+
console.print("• qBraid Lab environment, or")
|
|
377
|
+
console.print("• Valid qBraid API credentials configured")
|
|
378
|
+
console.print("\nTo configure credentials, run: [bold]qbraid configure[/bold]")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
console = Console()
|
|
382
|
+
environments = result.get("environments", [])
|
|
383
|
+
|
|
384
|
+
if not environments:
|
|
385
|
+
console.print("No environments available for installation.")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
# Check if we're on cloud or local
|
|
389
|
+
is_cloud = any(env.get("available_for_installation", False) for env in environments)
|
|
390
|
+
|
|
391
|
+
# Display header
|
|
392
|
+
if is_cloud:
|
|
393
|
+
status_text = "[bold green]✓ Available for installation[/bold green]"
|
|
394
|
+
else:
|
|
395
|
+
status_text = "[bold yellow]⚠ Not available (local environment)[/bold yellow]"
|
|
396
|
+
|
|
397
|
+
console.print(f"\n# Available qBraid Environments ({status_text})")
|
|
398
|
+
console.print(f"# Total: {len(environments)} environments")
|
|
399
|
+
console.print("#")
|
|
400
|
+
console.print("")
|
|
401
|
+
|
|
402
|
+
# Calculate column widths
|
|
403
|
+
max_name_len = max(len(env.get("displayName", env.get("name", ""))) for env in environments)
|
|
404
|
+
max_slug_len = max(len(env.get("slug", "")) for env in environments)
|
|
405
|
+
name_width = min(max_name_len + 2, 30)
|
|
406
|
+
slug_width = min(max_slug_len + 2, 20)
|
|
407
|
+
|
|
408
|
+
# Display environments
|
|
409
|
+
for env in environments:
|
|
410
|
+
name = env.get("displayName", env.get("name", "N/A"))
|
|
411
|
+
slug = env.get("slug", "N/A")
|
|
412
|
+
description = env.get("description", "No description")
|
|
413
|
+
available = env.get("available_for_installation", False)
|
|
414
|
+
|
|
415
|
+
# Truncate long names/slugs
|
|
416
|
+
if len(name) > 28:
|
|
417
|
+
name = name[:25] + "..."
|
|
418
|
+
if len(slug) > 18:
|
|
419
|
+
slug = slug[:15] + "..."
|
|
420
|
+
if len(description) > 60:
|
|
421
|
+
description = description[:57] + "..."
|
|
422
|
+
|
|
423
|
+
status_icon = "✓" if available else "⚠"
|
|
424
|
+
status_color = "green" if available else "yellow"
|
|
425
|
+
|
|
426
|
+
console.print(
|
|
427
|
+
f"[{status_color}]{status_icon}[/{status_color}] "
|
|
428
|
+
f"{name.ljust(name_width)} "
|
|
429
|
+
f"[dim]({slug.ljust(slug_width)})[/dim] "
|
|
430
|
+
f"{description}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Show usage hint
|
|
434
|
+
console.print("")
|
|
435
|
+
if is_cloud:
|
|
436
|
+
console.print("[dim]To install an environment, note its slug and use:[/dim]")
|
|
437
|
+
console.print("[dim] qbraid envs install <slug>[/dim]")
|
|
438
|
+
else:
|
|
439
|
+
console.print("[dim]Environment installation is only available on qBraid Lab.[/dim]")
|
|
440
|
+
console.print("[dim]Create custom environments with: qbraid envs create[/dim]")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@envs_app.command(name="install")
|
|
444
|
+
def envs_install(
|
|
445
|
+
env_slug: str = typer.Argument(..., help="Environment slug to install (from 'qbraid envs available')"),
|
|
446
|
+
temp: bool = typer.Option(
|
|
447
|
+
False, "--temp", "-t",
|
|
448
|
+
help="Install as temporary environment (faster, non-persistent)"
|
|
449
|
+
),
|
|
450
|
+
target_dir: str = typer.Option(
|
|
451
|
+
None, "--target", help="Custom target directory (defaults to ~/.qbraid/environments or /opt/environments)"
|
|
452
|
+
),
|
|
453
|
+
yes: bool = typer.Option(
|
|
454
|
+
False, "--yes", "-y",
|
|
455
|
+
help="Automatically overwrite existing environment without prompting"
|
|
456
|
+
),
|
|
457
|
+
):
|
|
458
|
+
"""Install a pre-built environment from cloud storage."""
|
|
459
|
+
|
|
460
|
+
async def install_environment_async():
|
|
461
|
+
"""Async wrapper for environment installation."""
|
|
462
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
463
|
+
from pathlib import Path
|
|
464
|
+
|
|
465
|
+
# Determine target directory
|
|
466
|
+
if target_dir:
|
|
467
|
+
install_dir = target_dir
|
|
468
|
+
elif temp:
|
|
469
|
+
install_dir = "/opt/environments"
|
|
470
|
+
else:
|
|
471
|
+
install_dir = str(Path.home() / ".qbraid" / "environments")
|
|
472
|
+
|
|
473
|
+
console = Console()
|
|
474
|
+
|
|
475
|
+
# Check if we're on qBraid Lab for non-temp installs
|
|
476
|
+
emc = EnvironmentManagerClient()
|
|
477
|
+
is_cloud = emc.running_in_lab()
|
|
478
|
+
|
|
479
|
+
if not temp and not is_cloud:
|
|
480
|
+
console.print("[red]❌ Environment installation requires qBraid Lab or use --temp flag[/red]")
|
|
481
|
+
console.print("💡 Try: [bold]qbraid envs install {env_slug} --temp[/bold]")
|
|
482
|
+
raise typer.Exit(1)
|
|
483
|
+
|
|
484
|
+
if temp and not is_cloud:
|
|
485
|
+
console.print("[yellow]⚠️ Local temporary install - environment won't persist after restart[/yellow]")
|
|
486
|
+
|
|
487
|
+
# Install environment
|
|
488
|
+
try:
|
|
489
|
+
console.print(f"🚀 Installing environment: [bold]{env_slug}[/bold]")
|
|
490
|
+
if temp:
|
|
491
|
+
console.print(f"📂 Target: [dim]{install_dir}[/dim] (temporary)")
|
|
492
|
+
else:
|
|
493
|
+
console.print(f"📂 Target: [dim]{install_dir}[/dim] (persistent)")
|
|
494
|
+
console.print("")
|
|
495
|
+
|
|
496
|
+
progress_columns = (
|
|
497
|
+
SpinnerColumn(),
|
|
498
|
+
TextColumn("{task.description}"),
|
|
499
|
+
BarColumn(),
|
|
500
|
+
DownloadColumn(),
|
|
501
|
+
TransferSpeedColumn(),
|
|
502
|
+
TimeElapsedColumn(),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
with Progress(*progress_columns, transient=True) as progress:
|
|
506
|
+
tasks: dict[str, int] = {}
|
|
507
|
+
|
|
508
|
+
def get_task(stage: str, description: str, total: Optional[int]) -> int:
|
|
509
|
+
task_id = tasks.get(stage)
|
|
510
|
+
if task_id is None:
|
|
511
|
+
task_total = total if total and total > 0 else None
|
|
512
|
+
task_id = progress.add_task(description, total=task_total)
|
|
513
|
+
tasks[stage] = task_id
|
|
514
|
+
return task_id
|
|
515
|
+
|
|
516
|
+
def progress_callback(stage: str, completed: int, total: int) -> None:
|
|
517
|
+
total_value = total if total and total > 0 else None
|
|
518
|
+
if stage == "download":
|
|
519
|
+
task_id = get_task("download", "Downloading environment...", total_value)
|
|
520
|
+
task = progress.tasks[task_id]
|
|
521
|
+
if total_value and (task.total is None or task.total == 0):
|
|
522
|
+
progress.update(task_id, total=total_value)
|
|
523
|
+
progress.update(task_id, completed=completed)
|
|
524
|
+
elif stage == "extract":
|
|
525
|
+
task_id = get_task("extract", "Extracting files...", total_value)
|
|
526
|
+
task = progress.tasks[task_id]
|
|
527
|
+
if total_value:
|
|
528
|
+
if task.total is None or task.total == 0:
|
|
529
|
+
progress.update(task_id, total=total_value)
|
|
530
|
+
progress.update(task_id, completed=completed)
|
|
531
|
+
else:
|
|
532
|
+
progress.update(task_id, completed=task.completed + 1)
|
|
533
|
+
|
|
534
|
+
result = await emc.install_environment_from_storage(
|
|
535
|
+
slug=env_slug,
|
|
536
|
+
target_dir=install_dir,
|
|
537
|
+
temp=temp,
|
|
538
|
+
overwrite=yes,
|
|
539
|
+
progress_callback=progress_callback,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
console.print("")
|
|
543
|
+
console.print(f"[green]✅ Installation completed![/green]")
|
|
544
|
+
console.print(f"📍 Location: [bold]{result['target_dir']}[/bold]")
|
|
545
|
+
if 'size_mb' in result:
|
|
546
|
+
console.print(f"💾 Size: [dim]{result['size_mb']:.1f} MB[/dim]")
|
|
547
|
+
if 'env_id' in result:
|
|
548
|
+
console.print(f"🆔 Env ID: [dim]{result['env_id']}[/dim]")
|
|
549
|
+
|
|
550
|
+
if temp:
|
|
551
|
+
console.print("[yellow]⚠️ Temporary environment - will be deleted on pod restart[/yellow]")
|
|
552
|
+
|
|
553
|
+
env_activation_id = result.get("env_id", env_slug)
|
|
554
|
+
console.print("")
|
|
555
|
+
console.print("🎯 Next steps:")
|
|
556
|
+
console.print(f"• Activate: [bold]qbraid envs activate {env_activation_id}[/bold]")
|
|
557
|
+
console.print(f"• List all: [bold]qbraid envs list[/bold]")
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
if isinstance(e, EnvironmentValidationError) and "already exists" in str(e):
|
|
561
|
+
console.print("[yellow]⚠️ Installation skipped: Environment already exists.[/yellow]")
|
|
562
|
+
else:
|
|
563
|
+
console.print(f"[red]❌ Installation failed: {e}[/red]")
|
|
564
|
+
raise typer.Exit(1)
|
|
565
|
+
# Run async function
|
|
566
|
+
import asyncio
|
|
567
|
+
try:
|
|
568
|
+
asyncio.run(install_environment_async())
|
|
569
|
+
except KeyboardInterrupt:
|
|
570
|
+
console = Console()
|
|
571
|
+
console.print("\n[yellow]⚠️ Installation cancelled by user[/yellow]")
|
|
572
|
+
raise typer.Exit(1)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@envs_app.command(name="publish")
|
|
576
|
+
def envs_publish(
|
|
577
|
+
slug: str = typer.Argument(..., help="Environment slug identifier"),
|
|
578
|
+
path: Optional[Path] = typer.Option(
|
|
579
|
+
None,
|
|
580
|
+
"--path",
|
|
581
|
+
"-p",
|
|
582
|
+
help="Path to environment directory (default: lookup from registry)"
|
|
583
|
+
),
|
|
584
|
+
overwrite: bool = typer.Option(
|
|
585
|
+
False,
|
|
586
|
+
"--overwrite",
|
|
587
|
+
"-o",
|
|
588
|
+
help="Overwrite existing published environment"
|
|
589
|
+
),
|
|
590
|
+
):
|
|
591
|
+
"""
|
|
592
|
+
Publish environment to qBraid cloud storage for global distribution.
|
|
593
|
+
|
|
594
|
+
This command packages and uploads your environment to make it available
|
|
595
|
+
for installation by other users via 'qbraid envs install'.
|
|
596
|
+
|
|
597
|
+
Examples:
|
|
598
|
+
$ qbraid envs publish my_custom_env
|
|
599
|
+
$ qbraid envs publish my_env --path ~/my_environment
|
|
600
|
+
$ qbraid envs publish my_env --overwrite
|
|
601
|
+
"""
|
|
602
|
+
from rich.console import Console
|
|
603
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
604
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
605
|
+
from qbraid_core.services.environments.registry import EnvironmentRegistryManager
|
|
606
|
+
|
|
607
|
+
console = Console()
|
|
608
|
+
|
|
609
|
+
# Determine environment path
|
|
610
|
+
if path:
|
|
611
|
+
env_path = path.expanduser().resolve()
|
|
612
|
+
if not env_path.exists():
|
|
613
|
+
console.print(f"[red]❌ Error:[/red] Path does not exist: {env_path}")
|
|
614
|
+
raise typer.Exit(1)
|
|
615
|
+
else:
|
|
616
|
+
# Look up from registry by slug
|
|
617
|
+
try:
|
|
618
|
+
registry_mgr = EnvironmentRegistryManager()
|
|
619
|
+
found = registry_mgr.find_by_slug(slug)
|
|
620
|
+
if not found:
|
|
621
|
+
console.print(f"[red]❌ Error:[/red] Environment with slug '{slug}' not found in registry")
|
|
622
|
+
console.print("\n[yellow]💡 Tip:[/yellow] Use --path to specify environment directory manually.")
|
|
623
|
+
raise typer.Exit(1)
|
|
624
|
+
env_id, env = found
|
|
625
|
+
env_path = Path(env.path)
|
|
626
|
+
except Exception as err:
|
|
627
|
+
console.print(f"[red]❌ Error:[/red] Failed to locate environment: {err}")
|
|
628
|
+
console.print("\n[yellow]💡 Tip:[/yellow] Use --path to specify environment directory manually.")
|
|
629
|
+
raise typer.Exit(1)
|
|
630
|
+
|
|
631
|
+
console.print(f"🚀 Publishing environment: [bold]{slug}[/bold]")
|
|
632
|
+
console.print(f"📂 Source: {env_path}\n")
|
|
633
|
+
|
|
634
|
+
# Define async function
|
|
635
|
+
async def publish_environment_async():
|
|
636
|
+
"""Async wrapper for environment publishing."""
|
|
637
|
+
client = EnvironmentManagerClient()
|
|
638
|
+
|
|
639
|
+
# Progress tracking
|
|
640
|
+
stages = {"archive": False, "upload": False}
|
|
641
|
+
|
|
642
|
+
def progress_callback(stage: str, completed: int, total: int):
|
|
643
|
+
"""Track progress across stages."""
|
|
644
|
+
stages[stage] = True
|
|
645
|
+
|
|
646
|
+
with Progress(
|
|
647
|
+
SpinnerColumn(),
|
|
648
|
+
TextColumn("[progress.description]{task.description}"),
|
|
649
|
+
BarColumn(),
|
|
650
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
651
|
+
console=console,
|
|
652
|
+
) as progress:
|
|
653
|
+
# Add tasks
|
|
654
|
+
archive_task = progress.add_task("📦 Creating archive...", total=100)
|
|
655
|
+
upload_task = progress.add_task("☁️ Uploading to cloud...", total=100)
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
# TODO: Update this when env_share_publish branch is merged
|
|
659
|
+
# This will use remote_publish_environment and handle directory renaming
|
|
660
|
+
raise NotImplementedError(
|
|
661
|
+
"Publish command will be updated when env_share_publish branch is merged. "
|
|
662
|
+
"It will use remote_publish_environment to get slug from API, "
|
|
663
|
+
"rename directory from env_id to slug, update paths, and upload."
|
|
664
|
+
)
|
|
665
|
+
# result = await client.publish_environment(
|
|
666
|
+
# slug=slug,
|
|
667
|
+
# env_path=str(env_path),
|
|
668
|
+
# overwrite=overwrite,
|
|
669
|
+
# progress_callback=progress_callback
|
|
670
|
+
# )
|
|
671
|
+
|
|
672
|
+
# Update progress bars
|
|
673
|
+
if stages.get("archive"):
|
|
674
|
+
progress.update(archive_task, completed=100)
|
|
675
|
+
if stages.get("upload"):
|
|
676
|
+
progress.update(upload_task, completed=100)
|
|
677
|
+
|
|
678
|
+
return result
|
|
679
|
+
|
|
680
|
+
except Exception as err:
|
|
681
|
+
raise err
|
|
682
|
+
|
|
683
|
+
# Run async function
|
|
684
|
+
import asyncio
|
|
685
|
+
try:
|
|
686
|
+
result = asyncio.run(publish_environment_async())
|
|
687
|
+
|
|
688
|
+
console.print(f"\n[green]✅ Published successfully![/green]")
|
|
689
|
+
console.print(f"📍 Bucket: {result.get('bucket', 'N/A')}")
|
|
690
|
+
console.print(f"📄 Path: {result.get('path', 'N/A')}")
|
|
691
|
+
console.print(f"\n🎯 Users can now install with:")
|
|
692
|
+
console.print(f" [bold cyan]qbraid envs install {slug}[/bold cyan]")
|
|
693
|
+
|
|
694
|
+
except KeyboardInterrupt:
|
|
695
|
+
console.print("\n[yellow]⚠️ Publishing cancelled by user[/yellow]")
|
|
696
|
+
raise typer.Exit(1)
|
|
697
|
+
except Exception as err:
|
|
698
|
+
console.print(f"\n[red]❌ Publishing failed:[/red] {err}")
|
|
699
|
+
raise typer.Exit(1)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@envs_app.command(name="add-path")
|
|
703
|
+
def envs_add_path(
|
|
704
|
+
path: Path = typer.Argument(..., exists=True, dir_okay=True, file_okay=False),
|
|
705
|
+
alias: Optional[str] = typer.Option(None, "--alias", "-a", help="Alias for the environment"),
|
|
706
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Name/slug for the environment"),
|
|
707
|
+
auto_confirm: bool = typer.Option(False, "--yes", "-y"),
|
|
708
|
+
):
|
|
709
|
+
"""
|
|
710
|
+
Register an external Python environment with qBraid.
|
|
711
|
+
|
|
712
|
+
This allows you to use existing Python environments (conda, venv, etc.)
|
|
713
|
+
with qBraid commands like kernel management and activation.
|
|
714
|
+
|
|
715
|
+
Examples:
|
|
716
|
+
$ qbraid envs add-path /path/to/my_env --alias myenv
|
|
717
|
+
$ qbraid envs add-path ~/conda/envs/quantum --name quantum_abc123
|
|
718
|
+
"""
|
|
719
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
720
|
+
|
|
721
|
+
def register():
|
|
722
|
+
emc = EnvironmentManagerClient()
|
|
723
|
+
result = emc.register_external_environment(
|
|
724
|
+
path=path,
|
|
725
|
+
name=alias or name or path.name,
|
|
726
|
+
)
|
|
727
|
+
return result
|
|
728
|
+
|
|
729
|
+
# Get info first to show user
|
|
730
|
+
try:
|
|
731
|
+
emc = EnvironmentManagerClient()
|
|
732
|
+
# Do a dry run to validate and get info
|
|
733
|
+
from qbraid_core.system.executables import is_valid_python
|
|
734
|
+
|
|
735
|
+
python_candidates = [
|
|
736
|
+
path / "bin" / "python",
|
|
737
|
+
path / "bin" / "python3",
|
|
738
|
+
path / "Scripts" / "python.exe",
|
|
739
|
+
]
|
|
740
|
+
|
|
741
|
+
python_path = None
|
|
742
|
+
for candidate in python_candidates:
|
|
743
|
+
if is_valid_python(candidate):
|
|
744
|
+
python_path = candidate
|
|
745
|
+
break
|
|
746
|
+
|
|
747
|
+
if not python_path:
|
|
748
|
+
handle_error(
|
|
749
|
+
error_type="ValidationError",
|
|
750
|
+
message=f"No valid Python executable found in {path}"
|
|
751
|
+
)
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
# Confirm with user
|
|
755
|
+
if not auto_confirm:
|
|
756
|
+
console = Console()
|
|
757
|
+
console.print("\n[bold]📦 Registering external environment:[/bold]")
|
|
758
|
+
console.print(f" Path: {path}")
|
|
759
|
+
console.print(f" Name: {alias or name or path.name}")
|
|
760
|
+
console.print(f" Python: {python_path}")
|
|
761
|
+
|
|
762
|
+
if not typer.confirm("\nProceed with registration?"):
|
|
763
|
+
typer.echo("❌ Registration cancelled.")
|
|
764
|
+
return
|
|
765
|
+
|
|
766
|
+
result = run_progress_task(
|
|
767
|
+
register,
|
|
768
|
+
description="Registering environment...",
|
|
769
|
+
error_message="Failed to register environment"
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
typer.echo(f"\n✅ Environment '{result['name']}' registered successfully!")
|
|
773
|
+
typer.echo(f" Env ID: {result['env_id']}")
|
|
774
|
+
typer.echo(f"\nYou can now:")
|
|
775
|
+
typer.echo(f" - Add kernel: qbraid kernels add {result['name']}")
|
|
776
|
+
typer.echo(f" - View in list: qbraid envs list")
|
|
777
|
+
|
|
778
|
+
except Exception as e:
|
|
779
|
+
handle_error(message=str(e))
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@envs_app.command(name="remove-path")
|
|
783
|
+
def envs_remove_path(
|
|
784
|
+
name: str = typer.Argument(..., help="Name or alias of environment to unregister"),
|
|
785
|
+
auto_confirm: bool = typer.Option(False, "--yes", "-y"),
|
|
786
|
+
):
|
|
787
|
+
"""
|
|
788
|
+
Unregister an external environment from qBraid.
|
|
789
|
+
|
|
790
|
+
This only removes the environment from qBraid's registry.
|
|
791
|
+
The actual environment files are NOT deleted.
|
|
792
|
+
"""
|
|
793
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
794
|
+
|
|
795
|
+
def unregister(slug: str):
|
|
796
|
+
emc = EnvironmentManagerClient()
|
|
797
|
+
result = emc.unregister_external_environment(slug)
|
|
798
|
+
return result
|
|
799
|
+
|
|
800
|
+
# Find environment
|
|
801
|
+
try:
|
|
802
|
+
from qbraid_core.services.environments.registry import EnvironmentRegistryManager
|
|
803
|
+
|
|
804
|
+
registry_mgr = EnvironmentRegistryManager()
|
|
805
|
+
# Try to find by name first, then by env_id
|
|
806
|
+
found = registry_mgr.find_by_name(name)
|
|
807
|
+
if not found:
|
|
808
|
+
found = registry_mgr.find_by_env_id(name)
|
|
809
|
+
|
|
810
|
+
if not found:
|
|
811
|
+
handle_error(
|
|
812
|
+
error_type="NotFoundError",
|
|
813
|
+
message=f"Environment '{name}' not found in registry. "
|
|
814
|
+
"Use name (if unique) or env_id to reference the environment."
|
|
815
|
+
)
|
|
816
|
+
return
|
|
817
|
+
|
|
818
|
+
env_id, entry = found
|
|
819
|
+
|
|
820
|
+
if entry.type != "external":
|
|
821
|
+
console = Console()
|
|
822
|
+
console.print(f"[yellow]⚠️ Warning: '{name}' is a qBraid-managed environment.[/yellow]")
|
|
823
|
+
console.print(f" Use 'qbraid envs remove --name {name}' to fully remove it.")
|
|
824
|
+
return
|
|
825
|
+
|
|
826
|
+
if not auto_confirm:
|
|
827
|
+
console = Console()
|
|
828
|
+
console.print(f"\n[yellow]⚠️ Unregistering environment '{entry.name}'[/yellow]")
|
|
829
|
+
console.print(f" Env ID: {env_id}")
|
|
830
|
+
console.print(f" Path: {entry.path}")
|
|
831
|
+
console.print(f" Type: {entry.type}")
|
|
832
|
+
console.print(f"\n Note: Files at {entry.path} will NOT be deleted.")
|
|
833
|
+
|
|
834
|
+
if not typer.confirm("\nProceed?"):
|
|
835
|
+
typer.echo("❌ Unregistration cancelled.")
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
result = run_progress_task(
|
|
839
|
+
unregister,
|
|
840
|
+
env_id, # Can be name or env_id - method handles both
|
|
841
|
+
description="Unregistering environment...",
|
|
842
|
+
error_message="Failed to unregister environment"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
typer.echo(f"✅ Environment '{name}' unregistered from qBraid.")
|
|
846
|
+
|
|
847
|
+
except Exception as e:
|
|
848
|
+
handle_error(message=str(e))
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@envs_app.command(name="sync")
|
|
852
|
+
def envs_sync():
|
|
853
|
+
"""
|
|
854
|
+
Synchronize environment registry with filesystem.
|
|
855
|
+
|
|
856
|
+
This will:
|
|
857
|
+
- Remove registry entries for deleted environments
|
|
858
|
+
- Auto-discover new environments in default paths
|
|
859
|
+
- Verify all registered paths still exist
|
|
860
|
+
"""
|
|
861
|
+
from qbraid_core.services.environments.client import EnvironmentManagerClient
|
|
862
|
+
|
|
863
|
+
def sync():
|
|
864
|
+
emc = EnvironmentManagerClient()
|
|
865
|
+
stats = emc.sync_registry()
|
|
866
|
+
return stats
|
|
867
|
+
|
|
868
|
+
result = run_progress_task(
|
|
869
|
+
sync,
|
|
870
|
+
description="Synchronizing environment registry...",
|
|
871
|
+
error_message="Failed to sync registry"
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
console = Console()
|
|
875
|
+
|
|
876
|
+
if result["discovered"] > 0:
|
|
877
|
+
console.print(f"[green]✅ Registry synced: {result['discovered']} new environment(s) discovered[/green]")
|
|
878
|
+
elif result["removed"] > 0:
|
|
879
|
+
console.print(f"[green]✅ Registry synced: {result['removed']} invalid entry(ies) removed[/green]")
|
|
880
|
+
else:
|
|
881
|
+
console.print("[green]✅ Registry synced: No changes detected[/green]")
|
|
882
|
+
|
|
883
|
+
# Show summary
|
|
884
|
+
console.print(f"\n[bold]Summary:[/bold]")
|
|
885
|
+
console.print(f" Verified: {result['verified']}")
|
|
886
|
+
console.print(f" Discovered: {result['discovered']}")
|
|
887
|
+
console.print(f" Removed: {result['removed']}")
|
|
888
|
+
|
|
889
|
+
|
|
263
890
|
if __name__ == "__main__":
|
|
264
891
|
envs_app()
|