nepher-cli 0.2.0__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.
- nepher_cli/__init__.py +3 -0
- nepher_cli/__main__.py +4 -0
- nepher_cli/cli.py +59 -0
- nepher_cli/commands/__init__.py +1 -0
- nepher_cli/commands/account.py +527 -0
- nepher_cli/commands/envhub.py +466 -0
- nepher_cli/commands/hackathon.py +760 -0
- nepher_cli/commands/simstore.py +49 -0
- nepher_cli/commands/tournament.py +651 -0
- nepher_cli/config.py +25 -0
- nepher_cli/core/__init__.py +1 -0
- nepher_cli/core/credentials.py +243 -0
- nepher_cli/core/http.py +76 -0
- nepher_cli/envhub/__init__.py +1 -0
- nepher_cli/envhub/cache.py +56 -0
- nepher_cli/envhub/config.py +176 -0
- nepher_cli/py.typed +0 -0
- nepher_cli/tournament/__init__.py +1 -0
- nepher_cli/tournament/agent_check.py +60 -0
- nepher_cli/tournament/api.py +100 -0
- nepher_cli/tournament/packer.py +50 -0
- nepher_cli/tournament/wallet.py +89 -0
- nepher_cli-0.2.0.dist-info/METADATA +193 -0
- nepher_cli-0.2.0.dist-info/RECORD +26 -0
- nepher_cli-0.2.0.dist-info/WHEEL +4 -0
- nepher_cli-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""envhub command group — list, download, upload, cache, view, config.
|
|
2
|
+
|
|
3
|
+
Talks directly to the envhub-backend REST API. The standalone ``nepher``
|
|
4
|
+
CLI (from the envhub package) is left unchanged; this group provides the
|
|
5
|
+
same operations from inside the unified npcli interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
import zipfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import httpx
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from nepher_cli.config import ENVHUB_BACKEND
|
|
22
|
+
from nepher_cli.core.credentials import get_auth_headers, get_stored_api_key
|
|
23
|
+
from nepher_cli.core.http import parse_error_body
|
|
24
|
+
from nepher_cli.envhub.cache import is_cached_env, list_cached_env_dirs, resolve_cache_dir
|
|
25
|
+
from nepher_cli.envhub.config import (
|
|
26
|
+
get_value as get_envhub_config_value,
|
|
27
|
+
list_values as list_envhub_config_values,
|
|
28
|
+
mask_secret as mask_envhub_config_secret,
|
|
29
|
+
parse_config_value,
|
|
30
|
+
reset_config as reset_envhub_config,
|
|
31
|
+
set_value as set_envhub_config_value,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
console = Console(stderr=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _headers(api_key: str | None) -> dict[str, str]:
|
|
38
|
+
h = get_auth_headers(api_key)
|
|
39
|
+
if not h:
|
|
40
|
+
ak = get_stored_api_key()
|
|
41
|
+
if ak:
|
|
42
|
+
h = {"X-API-Key": ak}
|
|
43
|
+
return h
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _base() -> str:
|
|
47
|
+
return ENVHUB_BACKEND.rstrip("/")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.group("envhub")
|
|
51
|
+
def envhub() -> None:
|
|
52
|
+
"""Manage Isaac Lab simulation environment bundles via EnvHub."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# list
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@envhub.command("list")
|
|
61
|
+
@click.option("--category", default=None, help="Filter by category.")
|
|
62
|
+
@click.option("--type", "env_type", type=click.Choice(["usd", "preset"]), default=None, help="Filter by type.")
|
|
63
|
+
@click.option("--benchmark", is_flag=True, help="Show only benchmark environments.")
|
|
64
|
+
@click.option("--eval-benchmarks", "eval_benchmarks", is_flag=True, help="Show only evaluation benchmarks.")
|
|
65
|
+
@click.option("--search", default=None, help="Full-text search query.")
|
|
66
|
+
@click.option("--limit", type=int, default=None, help="Maximum number of results.")
|
|
67
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
68
|
+
@click.option("--api-key", "api_key", default=None, envvar="NEPHER_API_KEY")
|
|
69
|
+
def envhub_list(
|
|
70
|
+
category: str | None,
|
|
71
|
+
env_type: str | None,
|
|
72
|
+
benchmark: bool,
|
|
73
|
+
eval_benchmarks: bool,
|
|
74
|
+
search: str | None,
|
|
75
|
+
limit: int | None,
|
|
76
|
+
output_json: bool,
|
|
77
|
+
api_key: str | None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""List available Isaac Lab environments."""
|
|
80
|
+
params: dict[str, Any] = {}
|
|
81
|
+
if category:
|
|
82
|
+
params["category"] = category
|
|
83
|
+
if env_type:
|
|
84
|
+
params["type"] = env_type
|
|
85
|
+
if benchmark:
|
|
86
|
+
params["benchmark"] = "true"
|
|
87
|
+
if search:
|
|
88
|
+
params["search"] = search
|
|
89
|
+
if limit:
|
|
90
|
+
params["limit"] = limit
|
|
91
|
+
|
|
92
|
+
endpoint = f"{_base()}/api/v1/envs/eval-benchmarks/" if eval_benchmarks else f"{_base()}/api/v1/envs/"
|
|
93
|
+
|
|
94
|
+
headers = _headers(api_key)
|
|
95
|
+
try:
|
|
96
|
+
r = httpx.get(endpoint, headers=headers, params=params, timeout=30.0)
|
|
97
|
+
except httpx.RequestError as e:
|
|
98
|
+
console.print(f"[red]Network error[/red]: {e}")
|
|
99
|
+
raise SystemExit(1) from e
|
|
100
|
+
|
|
101
|
+
if r.status_code != 200:
|
|
102
|
+
console.print(f"[red]{parse_error_body(r.text) or r.text.strip() or f'HTTP {r.status_code}'}[/red]")
|
|
103
|
+
raise SystemExit(1)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
data = r.json()
|
|
107
|
+
except Exception:
|
|
108
|
+
console.print("[red]Invalid JSON response.[/red]")
|
|
109
|
+
raise SystemExit(1)
|
|
110
|
+
|
|
111
|
+
if isinstance(data, list):
|
|
112
|
+
envs = data
|
|
113
|
+
elif isinstance(data, dict):
|
|
114
|
+
envs = data.get("environments", data.get("results", data.get("items", [])))
|
|
115
|
+
else:
|
|
116
|
+
envs = []
|
|
117
|
+
|
|
118
|
+
if output_json:
|
|
119
|
+
click.echo(json.dumps(envs, indent=2))
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if not envs:
|
|
123
|
+
console.print("[dim]No environments found.[/dim]")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
console.print(f"[bold]Found {len(envs)} environment(s):[/bold]\n")
|
|
127
|
+
for env in envs:
|
|
128
|
+
click.echo(f" {env.get('id', 'N/A')}")
|
|
129
|
+
click.echo(f" Name: {env.get('original_name', 'N/A')}")
|
|
130
|
+
click.echo(f" Version: {env.get('version', 'N/A')}")
|
|
131
|
+
click.echo(f" Category: {env.get('category', 'N/A')}")
|
|
132
|
+
click.echo(f" Type: {env.get('type', 'N/A')}")
|
|
133
|
+
click.echo(f" Status: {env.get('status', 'N/A')}")
|
|
134
|
+
if env.get("is_benchmark"):
|
|
135
|
+
click.echo(" Benchmark: Yes")
|
|
136
|
+
if env.get("description"):
|
|
137
|
+
click.echo(f" Description: {env.get('description')}")
|
|
138
|
+
click.echo()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# download
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@envhub.command("download")
|
|
147
|
+
@click.argument("env_id")
|
|
148
|
+
@click.option("--cache-dir", type=click.Path(), default=None, help="Override local cache directory.")
|
|
149
|
+
@click.option("--force", is_flag=True, help="Re-download even if already cached.")
|
|
150
|
+
@click.option("--api-key", "api_key", default=None, envvar="NEPHER_API_KEY")
|
|
151
|
+
def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: str | None) -> None:
|
|
152
|
+
"""Download an environment bundle and cache it locally.
|
|
153
|
+
|
|
154
|
+
The bundle is extracted to ~/.nepher/cache/<env_id>/ by default.
|
|
155
|
+
"""
|
|
156
|
+
cache_root = resolve_cache_dir(cache_dir)
|
|
157
|
+
env_cache = cache_root / env_id
|
|
158
|
+
|
|
159
|
+
if is_cached_env(env_cache) and not force:
|
|
160
|
+
console.print(f"[dim]Already cached:[/dim] {env_cache}")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
headers = _headers(api_key)
|
|
164
|
+
url = f"{_base()}/api/v1/envs/{env_id}/download"
|
|
165
|
+
console.print(f"Downloading [bold]{env_id}[/bold]...")
|
|
166
|
+
try:
|
|
167
|
+
with httpx.stream("GET", url, headers=headers, timeout=600.0, follow_redirects=True) as r:
|
|
168
|
+
if r.status_code != 200:
|
|
169
|
+
body = r.read().decode(errors="replace")
|
|
170
|
+
console.print(f"[red]{parse_error_body(body) or body.strip() or f'HTTP {r.status_code}'}[/red]")
|
|
171
|
+
raise SystemExit(1)
|
|
172
|
+
|
|
173
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
|
174
|
+
tmp_path = Path(tmp.name)
|
|
175
|
+
for chunk in r.iter_bytes(chunk_size=65536):
|
|
176
|
+
tmp.write(chunk)
|
|
177
|
+
except httpx.RequestError as e:
|
|
178
|
+
console.print(f"[red]Network error[/red]: {e}")
|
|
179
|
+
raise SystemExit(1) from e
|
|
180
|
+
|
|
181
|
+
console.print("Extracting bundle...")
|
|
182
|
+
env_cache.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
try:
|
|
184
|
+
with zipfile.ZipFile(tmp_path, "r") as zf:
|
|
185
|
+
zf.extractall(env_cache)
|
|
186
|
+
except zipfile.BadZipFile:
|
|
187
|
+
console.print("[red]Downloaded file is not a valid ZIP archive.[/red]")
|
|
188
|
+
shutil.rmtree(env_cache, ignore_errors=True)
|
|
189
|
+
tmp_path.unlink(missing_ok=True)
|
|
190
|
+
raise SystemExit(1)
|
|
191
|
+
finally:
|
|
192
|
+
tmp_path.unlink(missing_ok=True)
|
|
193
|
+
|
|
194
|
+
console.print(f"[green]Downloaded and cached:[/green] {env_cache}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# upload
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@envhub.command("upload")
|
|
203
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
204
|
+
@click.option("--category", required=True, help="Environment category (e.g. navigation, manipulation).")
|
|
205
|
+
@click.option("--benchmark", is_flag=True, help="Mark as a benchmark environment.")
|
|
206
|
+
@click.option("--force", is_flag=True, help="Upload even if a duplicate exists.")
|
|
207
|
+
@click.option("--thumbnail", type=click.Path(exists=True), default=None, help="Optional thumbnail image path.")
|
|
208
|
+
@click.option("--api-key", "api_key", default=None, envvar="NEPHER_API_KEY")
|
|
209
|
+
def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbnail: str | None, api_key: str | None) -> None:
|
|
210
|
+
"""Upload an Isaac Lab environment bundle.
|
|
211
|
+
|
|
212
|
+
PATH must be a directory containing a valid manifest.yaml, or a pre-built
|
|
213
|
+
.zip archive. Directories are zipped automatically before upload.
|
|
214
|
+
"""
|
|
215
|
+
headers = _headers(api_key)
|
|
216
|
+
if not headers:
|
|
217
|
+
console.print("[yellow]Not authenticated.[/yellow] Run [bold]npcli account login[/bold] or pass [bold]--api-key[/bold].")
|
|
218
|
+
raise SystemExit(1)
|
|
219
|
+
|
|
220
|
+
bundle_path = Path(path)
|
|
221
|
+
tmp_zip: Path | None = None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
if bundle_path.is_dir():
|
|
225
|
+
manifest = bundle_path / "manifest.yaml"
|
|
226
|
+
if not manifest.exists():
|
|
227
|
+
console.print("[red]Invalid bundle[/red]: manifest.yaml not found in directory.")
|
|
228
|
+
raise SystemExit(1)
|
|
229
|
+
console.print("Zipping bundle...")
|
|
230
|
+
tmp_fd, tmp_name = tempfile.mkstemp(suffix=".zip")
|
|
231
|
+
import os
|
|
232
|
+
os.close(tmp_fd)
|
|
233
|
+
tmp_zip = Path(tmp_name)
|
|
234
|
+
with zipfile.ZipFile(tmp_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
235
|
+
for file in bundle_path.rglob("*"):
|
|
236
|
+
if file.is_file():
|
|
237
|
+
zf.write(file, file.relative_to(bundle_path))
|
|
238
|
+
upload_path = tmp_zip
|
|
239
|
+
else:
|
|
240
|
+
upload_path = bundle_path
|
|
241
|
+
|
|
242
|
+
console.print(f"Uploading [bold]{bundle_path.name}[/bold]...")
|
|
243
|
+
data_fields = {"category": category}
|
|
244
|
+
if benchmark:
|
|
245
|
+
data_fields["benchmark"] = "true"
|
|
246
|
+
if force:
|
|
247
|
+
data_fields["force"] = "true"
|
|
248
|
+
|
|
249
|
+
with open(upload_path, "rb") as f:
|
|
250
|
+
files: dict[str, Any] = {"file": (upload_path.name, f, "application/zip")}
|
|
251
|
+
if thumbnail:
|
|
252
|
+
thumb_path = Path(thumbnail)
|
|
253
|
+
files["thumbnail"] = (thumb_path.name, open(thumbnail, "rb"), "image/jpeg")
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
r = httpx.post(
|
|
257
|
+
f"{_base()}/api/v1/envs/",
|
|
258
|
+
headers=headers,
|
|
259
|
+
data=data_fields,
|
|
260
|
+
files=files,
|
|
261
|
+
timeout=600.0,
|
|
262
|
+
)
|
|
263
|
+
except httpx.RequestError as e:
|
|
264
|
+
console.print(f"[red]Network error[/red]: {e}")
|
|
265
|
+
raise SystemExit(1) from e
|
|
266
|
+
|
|
267
|
+
if r.status_code in (200, 201):
|
|
268
|
+
body = r.json()
|
|
269
|
+
console.print("[green]Environment uploaded successfully.[/green]")
|
|
270
|
+
console.print(f" ID: {body.get('id', '?')}")
|
|
271
|
+
else:
|
|
272
|
+
console.print(f"[red]{parse_error_body(r.text) or r.text.strip() or f'HTTP {r.status_code}'}[/red]")
|
|
273
|
+
raise SystemExit(1)
|
|
274
|
+
|
|
275
|
+
finally:
|
|
276
|
+
if tmp_zip and tmp_zip.exists():
|
|
277
|
+
tmp_zip.unlink()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# cache sub-group
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@envhub.group("cache")
|
|
286
|
+
def envhub_cache() -> None:
|
|
287
|
+
"""Manage the local environment bundle cache."""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@envhub_cache.command("list")
|
|
291
|
+
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
292
|
+
def cache_list(cache_dir: str | None) -> None:
|
|
293
|
+
"""List locally cached environments."""
|
|
294
|
+
root = resolve_cache_dir(cache_dir)
|
|
295
|
+
entries = list_cached_env_dirs(root)
|
|
296
|
+
if not entries:
|
|
297
|
+
console.print("[dim]No cached environments.[/dim]")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
console.print(f"[bold]Cached environments ({len(entries)}):[/bold]")
|
|
301
|
+
for e in sorted(entries):
|
|
302
|
+
size = sum(f.stat().st_size for f in e.rglob("*") if f.is_file())
|
|
303
|
+
console.print(f" {e.name} ({size / 1024 / 1024:.1f} MB)")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@envhub_cache.command("clear")
|
|
307
|
+
@click.argument("env_id", required=False)
|
|
308
|
+
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
309
|
+
def cache_clear(env_id: str | None, cache_dir: str | None) -> None:
|
|
310
|
+
"""Clear cache — all environments or a specific one."""
|
|
311
|
+
root = resolve_cache_dir(cache_dir)
|
|
312
|
+
if env_id:
|
|
313
|
+
target = root / env_id
|
|
314
|
+
if target.exists():
|
|
315
|
+
shutil.rmtree(target)
|
|
316
|
+
console.print(f"[green]Cleared cache for[/green] {env_id}")
|
|
317
|
+
else:
|
|
318
|
+
console.print(f"[yellow]{env_id} is not cached.[/yellow]")
|
|
319
|
+
else:
|
|
320
|
+
if root.exists():
|
|
321
|
+
shutil.rmtree(root)
|
|
322
|
+
console.print("[green]Cleared all cached environments.[/green]")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@envhub_cache.command("info")
|
|
326
|
+
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
327
|
+
def cache_info(cache_dir: str | None) -> None:
|
|
328
|
+
"""Show cache size and location."""
|
|
329
|
+
root = resolve_cache_dir(cache_dir)
|
|
330
|
+
console.print(f"Cache directory: {root}")
|
|
331
|
+
if not root.exists():
|
|
332
|
+
console.print(" (empty — nothing cached yet)")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
entries = list_cached_env_dirs(root)
|
|
336
|
+
total = sum(f.stat().st_size for d in entries for f in d.rglob("*") if f.is_file())
|
|
337
|
+
console.print(f" Environments: {len(entries)}")
|
|
338
|
+
console.print(f" Total size: {total / 1024 / 1024:.2f} MB")
|
|
339
|
+
|
|
340
|
+
if entries:
|
|
341
|
+
click.echo("\n Environments:")
|
|
342
|
+
for e in sorted(entries):
|
|
343
|
+
size = sum(f.stat().st_size for f in e.rglob("*") if f.is_file())
|
|
344
|
+
click.echo(f" {e.name}: {size / 1024 / 1024:.2f} MB")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@envhub_cache.command("migrate")
|
|
348
|
+
@click.argument("new_path", type=click.Path())
|
|
349
|
+
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
350
|
+
def cache_migrate(new_path: str, cache_dir: str | None) -> None:
|
|
351
|
+
"""Move the local cache to a new directory."""
|
|
352
|
+
old_root = resolve_cache_dir(cache_dir)
|
|
353
|
+
new_root = Path(new_path)
|
|
354
|
+
if not old_root.exists():
|
|
355
|
+
console.print("[yellow]Nothing to migrate — cache is empty.[/yellow]")
|
|
356
|
+
return
|
|
357
|
+
new_root.parent.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
shutil.move(str(old_root), str(new_root))
|
|
359
|
+
console.print(f"[green]Cache migrated to[/green] {new_root}")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# view
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@envhub.command("view")
|
|
368
|
+
@click.argument("env_id")
|
|
369
|
+
@click.option("--category", default=None, help="Environment category (resolved from manifest if omitted).")
|
|
370
|
+
@click.option("--scene", default=None, help="Scene name or index.")
|
|
371
|
+
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
372
|
+
def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir: str | None) -> None:
|
|
373
|
+
"""View an environment in Isaac Sim (requires isaaclab on PATH).
|
|
374
|
+
|
|
375
|
+
The environment must be downloaded first via [bold]npcli envhub download[/bold].
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
from nepher.loader.registry import load_env, load_scene # type: ignore[import]
|
|
379
|
+
except ImportError:
|
|
380
|
+
console.print(
|
|
381
|
+
"[red]Isaac Lab not available.[/red]\n\n"
|
|
382
|
+
"The [bold]view[/bold] command requires Isaac Lab to be installed in the current Python "
|
|
383
|
+
"environment. Run it through Isaac Lab's Python interpreter:\n\n"
|
|
384
|
+
" [bold]isaaclab.bat -p -c 'import nepher_cli; nepher_cli.cli.main()' "
|
|
385
|
+
"envhub view <env_id>[/bold]\n\n"
|
|
386
|
+
"Or install Isaac Lab: https://isaac-sim.github.io/IsaacLab/"
|
|
387
|
+
)
|
|
388
|
+
raise SystemExit(1)
|
|
389
|
+
|
|
390
|
+
root = resolve_cache_dir(cache_dir)
|
|
391
|
+
env_path = root / env_id
|
|
392
|
+
if not is_cached_env(env_path):
|
|
393
|
+
console.print(
|
|
394
|
+
f"[yellow]{env_id} is not cached.[/yellow] "
|
|
395
|
+
f"Run [bold]npcli envhub download {env_id}[/bold] first."
|
|
396
|
+
)
|
|
397
|
+
raise SystemExit(1)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
env = load_env(env_id, category)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
console.print(f"[red]Failed to load environment[/red]: {e}")
|
|
403
|
+
raise SystemExit(1) from e
|
|
404
|
+
|
|
405
|
+
if not scene:
|
|
406
|
+
click.echo(f"Environment: {env_id}")
|
|
407
|
+
scenes = env.get_all_scenes() if hasattr(env, "get_all_scenes") else []
|
|
408
|
+
click.echo(f"Scenes ({len(scenes)}):")
|
|
409
|
+
for i, s in enumerate(scenes):
|
|
410
|
+
click.echo(f" [{i}] {s.name}")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
console.print(f"Launching scene [bold]{scene}[/bold] in Isaac Sim...")
|
|
414
|
+
# Actual rendering requires the full IsaacLab runtime; delegate back to the
|
|
415
|
+
# installed nepher package's view script which handles AppLauncher setup.
|
|
416
|
+
import subprocess, sys
|
|
417
|
+
subprocess.run([sys.executable, "-m", "nepher", "view", env_id, "--scene", scene], check=False)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# config sub-group
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@envhub.group("config")
|
|
426
|
+
def envhub_config() -> None:
|
|
427
|
+
"""Manage EnvHub configuration (shared with the ``nepher`` CLI)."""
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@envhub_config.command("get")
|
|
431
|
+
@click.argument("key")
|
|
432
|
+
def config_get(key: str) -> None:
|
|
433
|
+
"""Get a configuration value."""
|
|
434
|
+
val = get_envhub_config_value(key)
|
|
435
|
+
if val is None:
|
|
436
|
+
console.print(f"[yellow]Key '{key}' not set.[/yellow]")
|
|
437
|
+
else:
|
|
438
|
+
click.echo(mask_envhub_config_secret(key, val))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@envhub_config.command("set")
|
|
442
|
+
@click.argument("key")
|
|
443
|
+
@click.argument("value")
|
|
444
|
+
def config_set(key: str, value: str) -> None:
|
|
445
|
+
"""Set a configuration value."""
|
|
446
|
+
parsed = parse_config_value(value)
|
|
447
|
+
set_envhub_config_value(key, parsed)
|
|
448
|
+
display = mask_envhub_config_secret(key, parsed) if isinstance(parsed, str) else parsed
|
|
449
|
+
console.print(f"[green]Set[/green] {key} = {display}")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@envhub_config.command("list")
|
|
453
|
+
def config_list() -> None:
|
|
454
|
+
"""List EnvHub configuration values."""
|
|
455
|
+
console.print("[bold]Configuration:[/bold]")
|
|
456
|
+
for key, value in list_envhub_config_values().items():
|
|
457
|
+
click.echo(f" {key}: {value}")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@envhub_config.command("reset")
|
|
461
|
+
def config_reset() -> None:
|
|
462
|
+
"""Reset configuration to defaults."""
|
|
463
|
+
if reset_envhub_config():
|
|
464
|
+
console.print("[green]Configuration reset to defaults.[/green]")
|
|
465
|
+
else:
|
|
466
|
+
console.print("[dim]No configuration file to reset.[/dim]")
|