pow-cli 0.3.0__tar.gz → 0.4.0__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 (60) hide show
  1. {pow_cli-0.3.0 → pow_cli-0.4.0}/CHANGELOG.md +72 -1
  2. {pow_cli-0.3.0 → pow_cli-0.4.0}/PKG-INFO +2 -2
  3. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/init.py +51 -99
  4. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/ros.py +3 -0
  5. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/sim.py +50 -20
  6. pow_cli-0.4.0/pow_cli/common/installation.py +97 -0
  7. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/asset_manager.py +2 -1
  8. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/initializer.py +117 -52
  9. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/linter.py +5 -11
  10. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/models/pow_config.py +65 -20
  11. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/ros_manager.py +135 -9
  12. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/runner.py +21 -7
  13. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/vscode_settings.py +2 -2
  14. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/pow.template.toml +1 -1
  15. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/docker/Dockerfile.simros_jazzy +9 -4
  16. {pow_cli-0.3.0 → pow_cli-0.4.0}/pyproject.toml +2 -2
  17. pow_cli-0.4.0/tests/test_bump.py +30 -0
  18. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_config.py +65 -13
  19. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_init_cmd.py +83 -11
  20. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_initializer.py +56 -99
  21. pow_cli-0.4.0/tests/test_isaacsim_6_1_0.py +188 -0
  22. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_linter.py +26 -12
  23. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_prompt.py +14 -14
  24. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_ros_cmd.py +8 -0
  25. pow_cli-0.4.0/tests/test_ros_image_recovery.py +135 -0
  26. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_ros_manager.py +5 -5
  27. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_runner.py +37 -0
  28. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_sim_cmd.py +24 -17
  29. pow_cli-0.4.0/tests/test_sim_setup.py +238 -0
  30. pow_cli-0.4.0/tests/test_verify_isaacsim.py +156 -0
  31. pow_cli-0.3.0/tests/test_verify_isaacsim.py +0 -109
  32. {pow_cli-0.3.0 → pow_cli-0.4.0}/.gitignore +0 -0
  33. {pow_cli-0.3.0 → pow_cli-0.4.0}/LICENSE +0 -0
  34. {pow_cli-0.3.0 → pow_cli-0.4.0}/NOTICE +0 -0
  35. {pow_cli-0.3.0 → pow_cli-0.4.0}/README.md +0 -0
  36. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/__init__.py +0 -0
  37. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/__init__.py +0 -0
  38. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/asset.py +0 -0
  39. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/lint.py +0 -0
  40. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/main.py +0 -0
  41. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/python.py +0 -0
  42. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/cli/run.py +0 -0
  43. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/common/__init__.py +0 -0
  44. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/common/jsonc.py +0 -0
  45. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/common/prompt.py +0 -0
  46. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/common/utils.py +0 -0
  47. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/__init__.py +0 -0
  48. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/models/asset_profile.py +0 -0
  49. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/core/models/system_config.py +0 -0
  50. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/asset-registry/isaacsim_assets_5_1_0.toml +0 -0
  51. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/asset-registry/omniverse_assets.toml +0 -0
  52. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/example/asset-profile.example.toml +0 -0
  53. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/gitignore.template +0 -0
  54. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/data/system.template.toml +0 -0
  55. {pow_cli-0.3.0 → pow_cli-0.4.0}/pow_cli/main.py +0 -0
  56. {pow_cli-0.3.0 → pow_cli-0.4.0}/scripts/check_assets_urls.py +0 -0
  57. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/conftest.py +0 -0
  58. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_version.py +0 -0
  59. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_vscode_settings.py +0 -0
  60. {pow_cli-0.3.0 → pow_cli-0.4.0}/tests/test_xauthority.py +0 -0
@@ -2,7 +2,78 @@
2
2
 
3
3
  All notable changes to the `pow-cli` package will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.4.0] - 2026-09-14
6
+
7
+ ### Added
8
+
9
+ - **Isaac Sim 6.1.0 support.** `pow init` now offers 6.1.0, downloads its standalone
10
+ archive and selects the matching ROS workspace tag. Asset references use
11
+ `Assets/Isaac/6.1`. Isaac Sim 5.1.0 remains supported.
12
+ - **Linux aarch64 support.** `pow init` and `pow sim` download the
13
+ aarch64 build of 6.1.0 or 5.1.0 on ARM hosts such as DGX Spark, and the simulator launch and
14
+ compatibility-check commands accept that architecture. The bundled ROS Docker image remains
15
+ amd64-only: `pow init` skips its build on aarch64, and `pow ros` / `pow ros build`
16
+ report that limitation.
17
+ - **`pow sim` / `pow sim check` install missing prerequisites.** Both commands
18
+ now create missing global folders and `system.toml`, install the selected
19
+ supported release when absent, and prepare the asset-browser cache before
20
+ launching. No project or confirmation prompt is required. Incomplete installs
21
+ are backed up before repair; a failed repair restores the original contents.
22
+ - **ROS image rebuild prompt in `pow init`.** An existing `pow_simros_jazzy`
23
+ image without a matching simulator-version label used to stop initialization
24
+ with no rebuild option. Init now asks whether to rebuild it (default: No).
25
+ Accepting updates the existing tag using Docker's cache and preserves existing
26
+ containers and workspace files. Recreate containers explicitly to use the new image.
27
+
28
+ ### Changed
29
+
30
+ - `pow init` defaults new projects to `6.1.0` unless a global preference is set.
31
+ Explicit version selection takes precedence; keeping an existing project
32
+ configuration preserves its selected version.
33
+ - A missing version pinned in global `system.toml` is now installed by `pow sim`
34
+ and `pow sim check`, rather than falling back to another installed version.
35
+ - **ROS workspace and container compatibility checks.** Setup and launching now
36
+ check the selected 6.1.0 workspace, image label and running-container mount.
37
+ Unknown or incompatible state is reported with recovery guidance. Stale
38
+ container-mounted build directories are no longer deleted automatically.
39
+ - `pow lint` asset-version fixes use verified release mappings. Unknown versions are
40
+ not rewritten; changes to scenes still require `pow lint fix` and do not migrate
41
+ renamed assets or application APIs.
42
+ - The optional simulator lookup reads matching distribution metadata without
43
+ importing `isaacsim`, avoiding simulator startup during path discovery.
44
+ - Python 3.10 installs `tomli` for the existing `tomllib` fallback.
45
+
46
+ ### Fixed
47
+
48
+ - **`pow init` could not upgrade an existing project's simulator version.**
49
+ Choosing to update the project now opens the version picker instead of keeping
50
+ the old version. Explicit `--sim-version` changes are saved with the
51
+ corresponding simulator link and editor paths while preserving unrelated TOML
52
+ settings, profiles, and comments.
53
+ - **`pow sim check` accepted unsuccessful verdicts.** A `System checking result:`
54
+ line now succeeds only when its verdict is `PASSED`; failed or unrecognized
55
+ verdicts are errors, alongside missing results and nonzero process exits.
56
+ - **Docker inspection errors were treated as missing or incompatible images.**
57
+ Daemon and permission failures now report the underlying error without offering
58
+ a rebuild. Failed ROS builds stop initialization before custom-image building
59
+ and finalization.
60
+ - **`setup.py install is deprecated` warnings during colcon builds** — suppressed
61
+ in both automatic and manual builds inside the bundled ROS container
62
+ (requires rebuilding the image).
63
+ - `pow asset set` now suggests `pow asset unset` when the assets symlink already
64
+ exists, instead of telling the user to remove it manually.
65
+
66
+ ### Removed
67
+
68
+ - **Isaac Sim 6.0.1 installation support.** Projects pinned to it must select
69
+ 6.1.0 or 5.1.0 for initialization; an existing complete 6.0.1 installation can
70
+ still launch through `pow sim -v 6.0.1`.
71
+ - The mandatory `isaacsim` runtime dependency — installing and starting pow-cli
72
+ no longer requires a simulator package.
73
+
74
+ Automated coverage uses mocked simulator, GPU, network, and Docker operations.
75
+ Actual 6.1.0 checker output, GUI/headless launches, standalone Python, ROS topic
76
+ delivery, and DGX Spark workflows still require hardware validation.
6
77
 
7
78
  ---
8
79
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pow-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Command line interface for Isaac Powerpack
5
5
  Author: Titiwat Munintravong, Isaac Powerpack Contributors
6
6
  License-Expression: Apache-2.0
@@ -10,8 +10,8 @@ Requires-Python: >=3.10
10
10
  Requires-Dist: aiohttp>=3.11.11
11
11
  Requires-Dist: click>=8.1.7
12
12
  Requires-Dist: distro>=1.9.0
13
- Requires-Dist: isaacsim>=5.1.0.0
14
13
  Requires-Dist: rich
14
+ Requires-Dist: tomli>=2; python_version < '3.11'
15
15
  Requires-Dist: tomlkit>=0.14.0
16
16
  Description-Content-Type: text/markdown
17
17
 
@@ -6,9 +6,10 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
  from rich.panel import Panel
9
- from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
10
  from rich.prompt import Confirm
11
11
 
12
+ from ..common.installation import download_with_progress
12
13
  from ..common.prompt import ask_choice, ask_path
13
14
  from ..common.utils import console
14
15
  from ..core.initializer import Initializer
@@ -132,12 +133,13 @@ def _step3_global_folder(initializer: Initializer, global_path):
132
133
 
133
134
 
134
135
  def _version_choices() -> list[tuple[str, str]]:
135
- """Installable versions, latest first, annotated for the picker."""
136
+ """Versions installable on this host's architecture, latest first, annotated for the picker."""
136
137
  installed = set(PowConfig.installed_versions())
137
- latest = PowConfig.SUPPORTED_ISAACSIM_VERSIONS[0]
138
+ versions = PowConfig.versions_for_arch()
139
+ latest = versions[0] if versions else None
138
140
 
139
141
  choices = []
140
- for version in PowConfig.SUPPORTED_ISAACSIM_VERSIONS:
142
+ for version in versions:
141
143
  notes = []
142
144
  if version == latest:
143
145
  notes.append("latest")
@@ -170,10 +172,15 @@ def _resolve_sim_version(flag_version: str | None, config_version: str | None) -
170
172
  )
171
173
  return config_version
172
174
 
175
+ available = PowConfig.versions_for_arch()
176
+ default = PowConfig.configured_default_version() or (
177
+ available[0] if available else PowConfig.ISAACSIM_VERSION
178
+ )
179
+ PowConfig.release(default)
173
180
  return ask_choice(
174
181
  "Select Isaac Sim version",
175
182
  _version_choices(),
176
- default=PowConfig.ISAACSIM_VERSION,
183
+ default=default,
177
184
  )
178
185
 
179
186
 
@@ -182,97 +189,14 @@ def _step4_download_isaacsim(
182
189
  flag_version: str | None = None,
183
190
  config_version: str | None = None,
184
191
  ) -> dict | None:
185
- """Download Isaac Sim with a Rich progress bar. Returns result dict or None on error."""
192
+ """Select and install Isaac Sim. Return None on installation failure."""
186
193
  console.print("[bold blue][4/10] 📦 Isaac Sim App:[/bold blue] Select a version to install")
187
194
  version = _resolve_sim_version(flag_version, config_version)
188
- console.print(f" Installing Isaac Sim [bold]{version}[/bold]...")
189
-
190
- result = None
191
- error = None
192
-
193
- last_completed = 0
194
- last_time = time.time()
195
- current_phase = "download"
196
-
197
- with Progress(
198
- TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
199
- BarColumn(bar_width=40),
200
- "[progress.percentage]{task.percentage:>3.1f}%",
201
- "•",
202
- TextColumn("[bold cyan]{task.fields[speed]}"),
203
- console=console,
204
- refresh_per_second=10,
205
- ) as progress:
206
- download_task = progress.add_task(
207
- "download", filename=f"isaac-sim-{version}.zip", total=None, speed="0 MB/s"
208
- )
209
- extract_task = progress.add_task(
210
- "extract", filename="", total=None, speed=" ", visible=False
211
- )
212
-
213
- def progress_callback(completed, total):
214
- nonlocal last_completed, last_time, current_phase
215
- if not total:
216
- return
217
- now = time.time()
218
- if current_phase == "download":
219
- progress.update(download_task, total=total, completed=completed)
220
- elapsed = now - last_time
221
- if elapsed >= 0.5:
222
- speed_mb = ((completed - last_completed) / (1024 * 1024)) / elapsed
223
- progress.update(download_task, speed=f"{speed_mb:.1f} MB/s")
224
- last_completed = completed
225
- last_time = now
226
- elif current_phase == "extract":
227
- progress.update(extract_task, total=total, completed=completed)
228
-
229
- def status_callback(status):
230
- nonlocal current_phase, last_completed, last_time
231
- if status == "Downloading":
232
- current_phase = "download"
233
- elif status == "Extracting":
234
- current_phase = "extract"
235
- progress.update(download_task, visible=False)
236
- progress.update(
237
- extract_task,
238
- filename=f"isaac-sim-{version} [yellow](Extracting...)[/yellow]",
239
- speed=" ",
240
- completed=0,
241
- total=None,
242
- visible=True,
243
- )
244
- last_completed = 0
245
- last_time = time.time()
246
- elif status == "Extracted":
247
- progress.update(
248
- extract_task,
249
- filename=f"isaac-sim-{version} [green](Extracted → {version})[/green]",
250
- )
251
- time.sleep(1)
252
-
253
- try:
254
- result = initializer.download_isaacsim(
255
- version=version,
256
- progress_callback=progress_callback,
257
- status_callback=status_callback,
258
- )
259
- except Exception as e:
260
- error = e
261
-
262
- # Print outside the progress context so lines don't get consumed
263
- if error:
195
+ try:
196
+ return download_with_progress(initializer, version)
197
+ except Exception as error:
264
198
  console.print(f" [bold red]❌ Error:[/bold red] {error}")
265
199
  return None
266
- result.setdefault("version", version)
267
- if result["status"] == "Already installed":
268
- console.print(
269
- f" [yellow]✔[/yellow] Isaac Sim {version} is already installed at [dim]{result['path']}[/dim]"
270
- )
271
- else:
272
- console.print(
273
- f" [green]✔[/green] Downloaded and extracted to [dim]{result['path']}[/dim]"
274
- )
275
- return result
276
200
 
277
201
 
278
202
  def _step5_optimization(initializer: Initializer, isaacsim_path: str):
@@ -380,6 +304,13 @@ def _step6_ros_integration(
380
304
  f"via Isaac Sim internal libs."
381
305
  )
382
306
 
307
+ if not RosManager.docker_image_supported():
308
+ console.print(
309
+ f" [yellow]⊖[/yellow] Skipping ROS Docker image build: "
310
+ f"{RosManager.docker_image_unsupported_message()}"
311
+ )
312
+ return True, display_path
313
+
383
314
  # Build pow_simros Docker image
384
315
  simros_already_built = False
385
316
 
@@ -394,11 +325,26 @@ def _step6_ros_integration(
394
325
  line = state[len("simros_building:"):]
395
326
  simros_status.update(f"[bold green]pow_simros build:[/bold green] [dim]{line[:80]}[/dim]")
396
327
 
328
+ def confirm_simros_rebuild(image, version):
329
+ simros_status.stop()
330
+ try:
331
+ console.print(
332
+ f" ROS image '{image}' is unlabelled or targets another simulator. "
333
+ "Rebuilding updates its tag using Docker's cache. Existing containers "
334
+ "and workspace files are preserved; recreate containers explicitly "
335
+ "to use the rebuilt image."
336
+ )
337
+ return Confirm.ask(f" Rebuild '{image}' for Isaac Sim {version}?", default=False)
338
+ finally:
339
+ simros_status.start()
340
+
397
341
  with console.status("Building pow_simros image...") as simros_status:
398
342
  try:
399
343
  ros_mgr.build_simros_image(
400
344
  status_callback=simros_status_callback,
401
345
  ws_path=resolved_ws,
346
+ sim_version=sim_version,
347
+ confirm_rebuild=confirm_simros_rebuild,
402
348
  )
403
349
  except Exception as e:
404
350
  console.print(f" [bold red]❌ pow_simros Build Error:[/bold red] {e}")
@@ -425,7 +371,10 @@ def _step6_ros_integration(
425
371
 
426
372
  with console.status(f"Building custom image '{custom_image}'...") as custom_status:
427
373
  try:
428
- ros_mgr.build_custom_ros_image(status_callback=custom_status_callback)
374
+ ros_mgr.build_custom_ros_image(
375
+ status_callback=custom_status_callback,
376
+ sim_version=sim_version, ws_path=resolved_ws,
377
+ )
429
378
  except Exception as e:
430
379
  console.print(f" [bold red]❌ Custom ROS Build Error:[/bold red] {e}")
431
380
  console.print(" [yellow]⊖[/yellow] Skipping remaining steps due to build error.")
@@ -634,7 +583,8 @@ def init_cmd(sim_version: str | None):
634
583
 
635
584
  \b
636
585
  The Isaac Sim version comes from --sim-version, then from `[sim] version`
637
- in an existing pow.toml you chose to keep, then from an interactive prompt.
586
+ in an existing pow.toml you choose to keep, then from an interactive prompt
587
+ (global default or latest).
638
588
  """
639
589
  initializer = Initializer()
640
590
  config = initializer.get_config_path()
@@ -658,11 +608,13 @@ def init_cmd(sim_version: str | None):
658
608
  ros_forced = None
659
609
  ros_ws_forced = None
660
610
  version_forced = None
661
- if not override_pow_toml:
611
+ if Path("pow.toml").exists():
662
612
  try:
663
- ros_forced = initializer.config.get("enable_ros", False)
664
- ros_ws_forced = initializer.config.get("isaacsim_ros_ws", None)
665
- version_forced = initializer.config.get("version", None)
613
+ if not override_pow_toml or sim_version is not None:
614
+ ros_forced = initializer.config.get("enable_ros", False)
615
+ ros_ws_forced = initializer.config.get("isaacsim_ros_ws", None)
616
+ if not override_pow_toml:
617
+ version_forced = initializer.config.get("version", None)
666
618
  except Exception:
667
619
  pass
668
620
 
@@ -703,7 +655,7 @@ def init_cmd(sim_version: str | None):
703
655
  )
704
656
 
705
657
  finalized = _step10_finalize(
706
- initializer, override_pow_toml, ros_enabled, isaacsim_ros_ws,
658
+ initializer, override_pow_toml or sim_version is not None, ros_enabled, isaacsim_ros_ws,
707
659
  sim_version=resolved_version,
708
660
  )
709
661
 
@@ -83,6 +83,9 @@ def build_cmd(no_cache: bool):
83
83
  if config.project_root is None:
84
84
  raise click.ClickException("Not initialized. Run `pow init` first.")
85
85
 
86
+ if not RosManager.docker_image_supported():
87
+ raise click.ClickException(RosManager.docker_image_unsupported_message())
88
+
86
89
  if not config.ros_dockerfile:
87
90
  console.print(
88
91
  "[yellow]No `ros_dockerfile` set in pow.toml; nothing to build.[/yellow]\n"
@@ -2,7 +2,9 @@
2
2
 
3
3
  import click
4
4
  from rich.panel import Panel
5
+ from ..common.installation import download_with_progress
5
6
  from ..common.utils import console
7
+ from ..core.initializer import Initializer
6
8
  from ..core.models.pow_config import PowConfig
7
9
  from ..core.runner import Runner
8
10
 
@@ -15,26 +17,49 @@ def _default_sim_version() -> str:
15
17
  evaluates a callable default at parse time, so both the file read and the
16
18
  scan of ``<global>/isaacsim`` happen per invocation rather than at import.
17
19
  """
18
- pinned = PowConfig.configured_default_version()
19
- if not pinned:
20
- return PowConfig.resolve_installed_version()
21
-
22
- installed = PowConfig.installed_versions()
23
- # With nothing installed at all, honour the pin so the Runner's own
24
- # "not found" error names the version the user asked for.
25
- if installed and pinned not in installed:
26
- fallback = PowConfig.resolve_installed_version()
20
+ return PowConfig.configured_default_version() or PowConfig.resolve_installed_version()
21
+
22
+
23
+ def _ensure_sim_ready(version: str, *, check: bool = False) -> None:
24
+ """Complete init's global setup steps without reading project settings."""
25
+ global_path = PowConfig.resolve_global_path()
26
+ isaacsim_path = PowConfig.version_dir(version, global_path)
27
+ initializer = Initializer(global_path=global_path)
28
+ installed = initializer.isaacsim_ready(isaacsim_path, check=check)
29
+ if not installed:
30
+ # Reject unavailable versions before making any setup changes. Existing
31
+ # manually installed versions need not be in the download allowlist.
32
+ PowConfig.release(version)
33
+
34
+ global_ready = all(
35
+ (global_path / sub).is_dir() for sub in Initializer.GLOBAL_SUBFOLDERS
36
+ ) and (global_path / "system.toml").is_file()
37
+ if not global_ready:
27
38
  console.print(
28
- f"[yellow]⚠[/yellow] system.toml pins Isaac Sim {pinned}, which is "
29
- f"not installed; using {fallback}."
39
+ f"[blue]Preparing global configuration at [dim]{global_path}[/dim]...[/blue]"
30
40
  )
31
- return fallback
32
- return pinned
41
+ initializer.create_global_folder()
42
+ initializer.create_system_toml()
43
+
44
+ if not installed:
45
+ download_with_progress(initializer, version, check=check)
46
+ if not initializer.isaacsim_ready(isaacsim_path, check=check):
47
+ raise click.ClickException(
48
+ f"Isaac Sim installation at {isaacsim_path} is missing required scripts."
49
+ )
50
+
51
+ cache_path = initializer.asset_browser_cache_path(isaacsim_path)
52
+ if not cache_path.is_file():
53
+ console.print("[blue]Creating the missing Isaac Sim asset browser cache...[/blue]")
54
+ initializer.fix_asset_browser_cache(isaacsim_path)
55
+ if not cache_path.is_file():
56
+ raise click.ClickException(f"Isaac Sim asset browser cache is missing at {cache_path}.")
33
57
 
34
58
 
35
- def _guarded(action, **kwargs):
36
- """Run a Runner action, rendering unexpected errors as a Sim Error panel."""
59
+ def _guarded(action, *, check: bool = False, **kwargs):
60
+ """Ensure prerequisites, then run the action with consistent error output."""
37
61
  try:
62
+ _ensure_sim_ready(kwargs["version"], check=check)
38
63
  action(**kwargs)
39
64
  except click.ClickException:
40
65
  raise
@@ -102,6 +127,11 @@ def sim_group(ctx: click.Context):
102
127
  1. the -v/--version option
103
128
  2. [sim] default_version in .pow/system.toml
104
129
  3. the newest version installed under .pow/isaacsim/
130
+ 4. the latest release supported by this pow CLI
131
+
132
+ Missing global folders, configuration, supported installations and the
133
+ asset browser cache are set up automatically before launch or check.
134
+ A missing pinned version is installed. Setup failures stop the command.
105
135
 
106
136
  \b
107
137
  Subcommands:
@@ -134,7 +164,7 @@ def sim_group(ctx: click.Context):
134
164
  "--version",
135
165
  "sim_version",
136
166
  default=_default_sim_version,
137
- show_default="system.toml, else the installed version",
167
+ show_default="system.toml, else installed, else latest supported",
138
168
  help="Isaac Sim version to run.",
139
169
  )
140
170
  @click.option(
@@ -153,7 +183,7 @@ def sim_group(ctx: click.Context):
153
183
  )
154
184
  @click.pass_context
155
185
  def launch_cmd(ctx: click.Context, sim_version: str, ros_bridge: str, no_ros: bool):
156
- """Launch Isaac Sim, forwarding extra args to isaac-sim.sh."""
186
+ """Set up missing prerequisites, then launch Isaac Sim with extra args."""
157
187
  _guarded(
158
188
  Runner.run_sim,
159
189
  version=sim_version,
@@ -171,12 +201,12 @@ def launch_cmd(ctx: click.Context, sim_version: str, ros_bridge: str, no_ros: bo
171
201
  "--version",
172
202
  "sim_version",
173
203
  default=_default_sim_version,
174
- show_default="system.toml, else the installed version",
204
+ show_default="system.toml, else installed, else latest supported",
175
205
  help="Isaac Sim version to check.",
176
206
  )
177
207
  @click.pass_context
178
208
  def check_cmd(ctx: click.Context, sim_version: str):
179
- """Run the Isaac Sim compatibility check.
209
+ """Set up missing prerequisites, then run the Isaac Sim compatibility check.
180
210
 
181
211
  \b
182
212
  Runs .pow/isaacsim/<version>/isaac-sim.compatibility_check.sh, which
@@ -184,4 +214,4 @@ def check_cmd(ctx: click.Context, sim_version: str):
184
214
  project, and the script builds its own ROS environment - forward
185
215
  `-- --no-ros-env` to skip that step.
186
216
  """
187
- _guarded(Runner.run_sim_check, version=sim_version, extra_args=ctx.args)
217
+ _guarded(Runner.run_sim_check, check=True, version=sim_version, extra_args=ctx.args)
@@ -0,0 +1,97 @@
1
+ """Shared Isaac Sim installation progress for init and sim commands."""
2
+
3
+ import time
4
+
5
+ from rich.progress import BarColumn, Progress, TextColumn
6
+
7
+ from .utils import console
8
+ from ..core.initializer import Initializer
9
+
10
+
11
+ def download_with_progress(
12
+ initializer: Initializer, version: str, *, check: bool = False
13
+ ) -> dict:
14
+ """Install an already-selected version, propagating setup failures."""
15
+ console.print(f" Installing Isaac Sim [bold]{version}[/bold]...")
16
+
17
+ last_completed = 0
18
+ last_time = time.time()
19
+ current_phase = "download"
20
+
21
+ with Progress(
22
+ TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
23
+ BarColumn(bar_width=40),
24
+ "[progress.percentage]{task.percentage:>3.1f}%",
25
+ "•",
26
+ TextColumn("[bold cyan]{task.fields[speed]}"),
27
+ console=console,
28
+ refresh_per_second=10,
29
+ ) as progress:
30
+ download_task = progress.add_task(
31
+ "download", filename=f"isaac-sim-{version}.zip", total=None, speed="0 MB/s"
32
+ )
33
+ extract_task = progress.add_task(
34
+ "extract", filename="", total=None, speed=" ", visible=False
35
+ )
36
+
37
+ def progress_callback(completed, total):
38
+ nonlocal last_completed, last_time, current_phase
39
+ if not total:
40
+ return
41
+ now = time.time()
42
+ if current_phase == "download":
43
+ progress.update(download_task, total=total, completed=completed)
44
+ elapsed = now - last_time
45
+ if elapsed >= 0.5:
46
+ speed_mb = ((completed - last_completed) / (1024 * 1024)) / elapsed
47
+ progress.update(download_task, speed=f"{speed_mb:.1f} MB/s")
48
+ last_completed = completed
49
+ last_time = now
50
+ elif current_phase == "extract":
51
+ progress.update(extract_task, total=total, completed=completed)
52
+
53
+ def status_callback(status):
54
+ nonlocal current_phase, last_completed, last_time
55
+ if status == "Downloading":
56
+ current_phase = "download"
57
+ elif status == "Extracting":
58
+ current_phase = "extract"
59
+ progress.update(download_task, visible=False)
60
+ progress.update(
61
+ extract_task,
62
+ filename=f"isaac-sim-{version} [yellow](Extracting...)[/yellow]",
63
+ speed=" ",
64
+ completed=0,
65
+ total=None,
66
+ visible=True,
67
+ )
68
+ last_completed = 0
69
+ last_time = time.time()
70
+ elif status == "Extracted":
71
+ progress.update(
72
+ extract_task,
73
+ filename=f"isaac-sim-{version} [green](Extracted → {version})[/green]",
74
+ )
75
+ time.sleep(1)
76
+
77
+ result = initializer.download_isaacsim(
78
+ version=version,
79
+ progress_callback=progress_callback,
80
+ status_callback=status_callback,
81
+ check=check,
82
+ )
83
+
84
+ # Print outside the progress context so lines don't get consumed.
85
+ result.setdefault("version", version)
86
+ if result["status"] == "Already installed":
87
+ console.print(
88
+ f" [yellow]✔[/yellow] Isaac Sim {version} is already installed at [dim]{result['path']}[/dim]"
89
+ )
90
+ else:
91
+ console.print(
92
+ f" [green]✔[/green] Downloaded and extracted to [dim]{result['path']}[/dim]"
93
+ )
94
+ if result.get("backup"):
95
+ console.print(f" Previous installation preserved at [dim]{result['backup']}[/dim]")
96
+ return result
97
+
@@ -537,7 +537,8 @@ class AssetManager:
537
537
  if assets_symlink.is_symlink():
538
538
  raise AssetError(
539
539
  f"'{assets_symlink}' is already a symlink.\n"
540
- "Remove it manually before running this command."
540
+ "Run 'pow asset unset' to remove the current asset configuration "
541
+ "before running this command again."
541
542
  )
542
543
 
543
544
  @staticmethod