pow-cli 0.2.0rc2__tar.gz → 0.3.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 (61) hide show
  1. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/CHANGELOG.md +141 -0
  2. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/PKG-INFO +3 -5
  3. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/init.py +253 -47
  4. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/lint.py +3 -9
  5. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/main.py +2 -4
  6. pow_cli-0.3.0/pow_cli/cli/sim.py +187 -0
  7. pow_cli-0.3.0/pow_cli/common/jsonc.py +332 -0
  8. pow_cli-0.3.0/pow_cli/common/prompt.py +282 -0
  9. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/asset_manager.py +7 -1
  10. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/initializer.py +237 -103
  11. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/linter.py +155 -27
  12. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/models/pow_config.py +135 -6
  13. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/models/system_config.py +26 -4
  14. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/ros_manager.py +95 -32
  15. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/runner.py +93 -51
  16. pow_cli-0.3.0/pow_cli/core/vscode_settings.py +294 -0
  17. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/pow.template.toml +1 -1
  18. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/system.template.toml +5 -0
  19. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pyproject.toml +2 -4
  20. pow_cli-0.3.0/tests/conftest.py +11 -0
  21. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_config.py +119 -7
  22. pow_cli-0.3.0/tests/test_init_cmd.py +410 -0
  23. pow_cli-0.3.0/tests/test_initializer.py +466 -0
  24. pow_cli-0.3.0/tests/test_linter.py +147 -0
  25. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_prompt.py +149 -1
  26. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_ros_manager.py +77 -3
  27. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_runner.py +143 -4
  28. pow_cli-0.3.0/tests/test_sim_cmd.py +233 -0
  29. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_verify_isaacsim.py +45 -18
  30. pow_cli-0.3.0/tests/test_vscode_settings.py +370 -0
  31. pow_cli-0.3.0/tests/test_xauthority.py +105 -0
  32. pow_cli-0.2.0rc2/pow_cli/cli/check.py +0 -25
  33. pow_cli-0.2.0rc2/pow_cli/cli/sim.py +0 -67
  34. pow_cli-0.2.0rc2/pow_cli/common/prompt.py +0 -136
  35. pow_cli-0.2.0rc2/tests/test_check_cmd.py +0 -47
  36. pow_cli-0.2.0rc2/tests/test_init_cmd.py +0 -117
  37. pow_cli-0.2.0rc2/tests/test_initializer.py +0 -83
  38. pow_cli-0.2.0rc2/tests/test_sim_cmd.py +0 -71
  39. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/.gitignore +0 -0
  40. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/LICENSE +0 -0
  41. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/NOTICE +0 -0
  42. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/README.md +0 -0
  43. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/__init__.py +0 -0
  44. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/__init__.py +0 -0
  45. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/asset.py +0 -0
  46. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/python.py +0 -0
  47. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/ros.py +0 -0
  48. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/cli/run.py +0 -0
  49. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/common/__init__.py +0 -0
  50. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/common/utils.py +0 -0
  51. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/__init__.py +0 -0
  52. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/core/models/asset_profile.py +0 -0
  53. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/asset-registry/isaacsim_assets_5_1_0.toml +0 -0
  54. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/asset-registry/omniverse_assets.toml +0 -0
  55. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/example/asset-profile.example.toml +0 -0
  56. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/data/gitignore.template +0 -0
  57. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/docker/Dockerfile.simros_jazzy +0 -0
  58. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/pow_cli/main.py +0 -0
  59. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/scripts/check_assets_urls.py +0 -0
  60. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_ros_cmd.py +0 -0
  61. {pow_cli-0.2.0rc2 → pow_cli-0.3.0}/tests/test_version.py +0 -0
@@ -2,6 +2,147 @@
2
2
 
3
3
  All notable changes to the `pow-cli` package will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ---
8
+
9
+ ## [0.3.0] - 2026-09-07
10
+
11
+ ### Added
12
+
13
+ - **`pow lint` rule 4 — Isaac asset version mismatch.** Asset references pointing
14
+ at an `Assets/Isaac/<major>.<minor>` tree other than the one `[sim] version`
15
+ reads from are reported, and rewritten by `pow lint fix` (`version = "6.0.1"`
16
+ → `Assets/Isaac/6.0`). NVIDIA keeps every release's asset tree online, so a
17
+ stage authored against 5.1 kept loading 5.1 assets after the project moved to
18
+ 6.0.1 with nothing to show for it. The target version is derived from the
19
+ configured version rather than tabulated, applies to every reference form (S3
20
+ URLs, `pow-assets`/`user-home` aliases, relative paths), and is skipped when
21
+ `pow.toml` sets no version
22
+
23
+ - **Isaac Sim 6.0.1 support.** `pow init` step 4 now offers the installable
24
+ versions as an arrow-key picker — latest first, marking the latest release and
25
+ any already installed — or takes the version from the new `--sim-version`
26
+ flag, or from `[sim] version` in an existing `pow.toml` you chose to keep. The
27
+ picker falls back to a typed prompt when there is no terminal (piped stdin,
28
+ CI), so the command stays scriptable. Releases live in
29
+ `PowConfig.ISAACSIM_RELEASES`, which pins each version's download URL — 6.0.1
30
+ is served from `downloads.isaacsim.nvidia.com`, not the host used for 5.1.0 —
31
+ and the matching `IsaacSim-<version>` ROS workspace ref cloned during setup.
32
+ - `pow sim` / `pow sim check` default `-v` to the Isaac Sim version actually
33
+ installed under `.pow/isaacsim/` instead of a fixed constant.
34
+ - **`[sim] default_version` in `~/.pow/system.toml`** — pin the version `pow sim`
35
+ and `pow sim check` use when `-v` is omitted. Resolution order is `-v`, then
36
+ this key, then the newest version installed under `.pow/isaacsim/`; an empty
37
+ value means auto-detect. A pinned version that is not installed prints a
38
+ warning and falls back to the newest installed one. New `system.toml` files
39
+ are created with the key present and empty; existing files are left untouched
40
+ and read as unset.
41
+ - `pow sim --help` now lists the options bare `pow sim` accepts (`-v/--version`,
42
+ `--ros`, `--no-ros`), which previously showed up only under
43
+ `pow sim launch --help`.
44
+ - **`pow sim check`** — run the Isaac Sim compatibility check from the managed
45
+ installation (`.pow/isaacsim/<version>/isaac-sim.compatibility_check.sh`), with
46
+ `-v/--version` to pick the version. Needs no project and no extra pip
47
+ dependency; the script sets up its own ROS environment (forward
48
+ `-- --no-ros-env` to skip it). `pow sim` is now a command group whose default
49
+ subcommand is `launch` — bare `pow sim [args...]` still launches Isaac Sim and
50
+ forwards raw arguments exactly as before, and `pow sim launch` is the explicit
51
+ form
52
+
53
+ ### Changed
54
+
55
+ - **`pow init` no longer replaces an existing `pow.toml`.** Choosing to update
56
+ the configuration used to overwrite the file with the bundled template, losing
57
+ custom `exts`, `raw_args`, `ros_bridge`, every `[[profiles]]` block and all
58
+ comments. It now patches only the three settings init asks about — `version`,
59
+ `enable_ros`, `isaacsim_ros_ws` — and leaves the rest of the document, key
60
+ order and comments included, exactly as written. Step 10 lists what changed
61
+ (`version: 5.1.0 → 6.0.1`), and says the file is already up to date when
62
+ nothing moved, in which case it is not rewritten at all. Keys the template has
63
+ and your file lacks are deliberately not back-filled. The step-2 prompt is
64
+ reworded from "override" to "Update settings in existing pow.toml?" to match.
65
+ - The `_isaacsim` symlink is re-pointed when `pow init` installs a different
66
+ version, instead of being left on the previous installation. A real directory
67
+ at `_isaacsim` is never deleted; it is reported as an error.
68
+ - `pow init` writes the selected `version` into the generated `pow.toml`.
69
+ - Isaac Sim's bundled ROS 2 libraries are located by probing
70
+ `exts/isaacsim.ros2.core/<distro>/lib` and then
71
+ `exts/isaacsim.ros2.bridge/<distro>/lib`, so ROS works on 6.0.1, which renamed
72
+ the extension.
73
+
74
+ ### Security
75
+
76
+ - **ROS containers no longer disable X11 access control with `xhost +`.** When
77
+ `DISPLAY` is set, `pow ros` now copies only the current display's Xauthority
78
+ record into a private temporary file, mounts it read-only in the container,
79
+ and sets the container's `XAUTHORITY` to that file. The host's authority file
80
+ and X server access-control settings are left unchanged, and the temporary
81
+ credentials are removed when the Docker client exits. Headless launches skip
82
+ X11 setup; GUI launches fail with an actionable message when `xauth` or valid
83
+ display credentials are unavailable. Existing running containers must be
84
+ recreated to receive the new authentication mount.
85
+ - An Isaac Sim version can no longer escape `.pow/isaacsim/`: versions used as a
86
+ path component are validated, and download URLs are only ever read from the
87
+ release registry, never built from a supplied version string.
88
+ - Archive members are chmod-ed via the path `zipfile` actually wrote rather than
89
+ a recomputed one, so a crafted zip cannot change permissions outside the
90
+ install directory; setuid/setgid bits in the archive are dropped.
91
+
92
+ ### Fixed
93
+
94
+ - A `pow.toml` that is not valid TOML is reported as
95
+ `pow.toml is not valid TOML: <parse error>` with a pointer to fix or delete it,
96
+ instead of a raw `tomllib` traceback. No command rewrites the file to make it
97
+ parse.
98
+ - An interrupted Isaac Sim download no longer leaves a truncated zip that the
99
+ next `pow init` mistakes for a complete archive: downloads land on a `.part`
100
+ file and are renamed only once finished.
101
+ - Execute permissions are restored before `post_install.sh` runs, so an archive
102
+ without Unix mode bits no longer fails the install.
103
+ - `pow asset` resolves the `.kit` file from the project's configured Isaac Sim
104
+ version instead of a hardcoded `5.1.0` path.
105
+ - **`pow sim check` could report nothing at all** — inside Isaac Sim's bundled
106
+ Python the check extension failed with `ModuleNotFoundError: No module named
107
+ 'packaging'` (then `'setuptools'`) and never produced a verdict. The vendor
108
+ launcher `isaac-sim.compatibility_check.sh` never sources
109
+ `setup_python_env.sh`, so whether `packaging` was importable depended on kit's
110
+ extension load order. pow now puts the installation's own bundled module
111
+ directories on `PYTHONPATH` and no longer forwards the host `PYTHONPATH` (which
112
+ typically targets another Python version, the same reason the ROS bridge
113
+ environment strips it)
114
+ - **`pow sim check` exited 0 when the check produced no result** — Isaac Sim exits
115
+ 0 even when the check app fails to start. The command now streams the output and
116
+ fails with a clear message if no `System checking result:` line appears; a
117
+ non-zero exit code from the check still takes priority
118
+
119
+ ### Removed
120
+
121
+ - **`pow check`** — the standalone Isaac Sim compatibility check command has been
122
+ removed, replaced by `pow sim check` above. It ran the check through the pip
123
+ `isaacsim` entry point, so pow-cli no longer depends on the
124
+ `isaacsim[compatibility-check]` extra and `isaacsim-app` is no longer installed
125
+ as a transitive dependency
126
+ - The `packaging` and `setuptools` runtime dependencies, which existed only for
127
+ that pip-based check. `pow sim check` reads those modules from the Isaac Sim
128
+ installation instead
129
+
130
+ ---
131
+
132
+ ## [0.2.0] - 2026-07-28
133
+
134
+ Stable release of the 0.2.0 line, promoted from `0.2.0-rc.2` with no functional
135
+ changes.
136
+
137
+ Everything new since `0.1.1` is listed in the two release candidate entries
138
+ below — headline items: the new `pow sim` and `pow ros build` commands, the
139
+ `ros_bridge` and `ros_docker_image` config keys, Jazzy-only ROS 2 support
140
+ (Humble removed), a faster `pow init` that no longer builds the ROS workspace,
141
+ and fixes for `rosdep install`, `cpu_performance_mode`, and `ros2` tab
142
+ completion inside the container.
143
+
144
+ ---
145
+
5
146
  ## [0.2.0-rc.2] - 2026-07-28
6
147
 
7
148
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pow-cli
3
- Version: 0.2.0rc2
3
+ Version: 0.3.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,10 +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[compatibility-check]>=5.1.0.0
14
- Requires-Dist: packaging>=26.0
13
+ Requires-Dist: isaacsim>=5.1.0.0
15
14
  Requires-Dist: rich
16
- Requires-Dist: setuptools>=82.0.1
17
15
  Requires-Dist: tomlkit>=0.14.0
18
16
  Description-Content-Type: text/markdown
19
17
 
@@ -1,5 +1,6 @@
1
1
  """Init command implementation."""
2
2
 
3
+ import json
3
4
  import time
4
5
  from pathlib import Path
5
6
 
@@ -8,9 +9,10 @@ from rich.panel import Panel
8
9
  from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
9
10
  from rich.prompt import Confirm
10
11
 
11
- from ..common.prompt import ask_path
12
+ from ..common.prompt import ask_choice, ask_path
12
13
  from ..common.utils import console
13
14
  from ..core.initializer import Initializer
15
+ from ..core.models.pow_config import PowConfig
14
16
  from ..core.ros_manager import RosManager
15
17
 
16
18
 
@@ -87,11 +89,14 @@ def _step2_check_existing_config(initializer: Initializer) -> bool:
87
89
  "[yellow]Found existing pow.toml[/yellow]"
88
90
  )
89
91
  override = Confirm.ask(
90
- " Do you want to override existing pow.toml?",
92
+ " Update settings in existing pow.toml?",
91
93
  default=False,
92
94
  )
93
95
  if override:
94
- console.print(" [green]Proceeding and will override pow.toml.[/green]")
96
+ console.print(
97
+ " [green]Will update only version, enable_ros and isaacsim_ros_ws.[/green]"
98
+ )
99
+ console.print(" [dim]Your other settings and comments are kept.[/dim]")
95
100
  else:
96
101
  console.print(" [yellow]Proceeding with existing pow.toml.[/yellow]")
97
102
  initializer.read_config()
@@ -126,9 +131,61 @@ def _step3_global_folder(initializer: Initializer, global_path):
126
131
  )
127
132
 
128
133
 
129
- def _step4_download_isaacsim(initializer: Initializer) -> dict | None:
134
+ def _version_choices() -> list[tuple[str, str]]:
135
+ """Installable versions, latest first, annotated for the picker."""
136
+ installed = set(PowConfig.installed_versions())
137
+ latest = PowConfig.SUPPORTED_ISAACSIM_VERSIONS[0]
138
+
139
+ choices = []
140
+ for version in PowConfig.SUPPORTED_ISAACSIM_VERSIONS:
141
+ notes = []
142
+ if version == latest:
143
+ notes.append("latest")
144
+ if version in installed:
145
+ notes.append("installed")
146
+ choices.append((version, ", ".join(notes)))
147
+ return choices
148
+
149
+
150
+ def _resolve_sim_version(flag_version: str | None, config_version: str | None) -> str:
151
+ """Decide which Isaac Sim version to install.
152
+
153
+ Precedence: ``--sim-version`` flag, then ``[sim] version`` from an existing
154
+ pow.toml the user chose to keep, then an interactive prompt.
155
+ """
156
+ if flag_version:
157
+ console.print(
158
+ f" Using version from --sim-version: [bold green]{flag_version}[/bold green]"
159
+ )
160
+ return flag_version
161
+
162
+ if config_version:
163
+ if config_version not in PowConfig.SUPPORTED_ISAACSIM_VERSIONS:
164
+ raise click.ClickException(
165
+ f"Unsupported Isaac Sim version '{config_version}' in pow.toml. "
166
+ f"Supported versions: {', '.join(PowConfig.SUPPORTED_ISAACSIM_VERSIONS)}."
167
+ )
168
+ console.print(
169
+ f" Using version from pow.toml: [bold green]{config_version}[/bold green]"
170
+ )
171
+ return config_version
172
+
173
+ return ask_choice(
174
+ "Select Isaac Sim version",
175
+ _version_choices(),
176
+ default=PowConfig.ISAACSIM_VERSION,
177
+ )
178
+
179
+
180
+ def _step4_download_isaacsim(
181
+ initializer: Initializer,
182
+ flag_version: str | None = None,
183
+ config_version: str | None = None,
184
+ ) -> dict | None:
130
185
  """Download Isaac Sim with a Rich progress bar. Returns result dict or None on error."""
131
- console.print("[bold blue][4/10] 📦 Isaac Sim App:[/bold blue] Installing Isaac Sim 5.1.0...")
186
+ console.print("[bold blue][4/10] 📦 Isaac Sim App:[/bold blue] Select a version to install")
187
+ version = _resolve_sim_version(flag_version, config_version)
188
+ console.print(f" Installing Isaac Sim [bold]{version}[/bold]...")
132
189
 
133
190
  result = None
134
191
  error = None
@@ -147,7 +204,7 @@ def _step4_download_isaacsim(initializer: Initializer) -> dict | None:
147
204
  refresh_per_second=10,
148
205
  ) as progress:
149
206
  download_task = progress.add_task(
150
- "download", filename="isaac-sim-5.1.0.zip", total=None, speed="0 MB/s"
207
+ "download", filename=f"isaac-sim-{version}.zip", total=None, speed="0 MB/s"
151
208
  )
152
209
  extract_task = progress.add_task(
153
210
  "extract", filename="", total=None, speed=" ", visible=False
@@ -178,7 +235,7 @@ def _step4_download_isaacsim(initializer: Initializer) -> dict | None:
178
235
  progress.update(download_task, visible=False)
179
236
  progress.update(
180
237
  extract_task,
181
- filename="isaac-sim-5.1.0 [yellow](Extracting...)[/yellow]",
238
+ filename=f"isaac-sim-{version} [yellow](Extracting...)[/yellow]",
182
239
  speed=" ",
183
240
  completed=0,
184
241
  total=None,
@@ -189,12 +246,13 @@ def _step4_download_isaacsim(initializer: Initializer) -> dict | None:
189
246
  elif status == "Extracted":
190
247
  progress.update(
191
248
  extract_task,
192
- filename="isaac-sim-5.1.0 [green](Extracted → 5.1.0)[/green]",
249
+ filename=f"isaac-sim-{version} [green](Extracted → {version})[/green]",
193
250
  )
194
251
  time.sleep(1)
195
252
 
196
253
  try:
197
254
  result = initializer.download_isaacsim(
255
+ version=version,
198
256
  progress_callback=progress_callback,
199
257
  status_callback=status_callback,
200
258
  )
@@ -205,9 +263,10 @@ def _step4_download_isaacsim(initializer: Initializer) -> dict | None:
205
263
  if error:
206
264
  console.print(f" [bold red]❌ Error:[/bold red] {error}")
207
265
  return None
266
+ result.setdefault("version", version)
208
267
  if result["status"] == "Already installed":
209
268
  console.print(
210
- f" [yellow]✔[/yellow] Isaac Sim 5.1.0 is already installed at [dim]{result['path']}[/dim]"
269
+ f" [yellow]✔[/yellow] Isaac Sim {version} is already installed at [dim]{result['path']}[/dim]"
211
270
  )
212
271
  else:
213
272
  console.print(
@@ -232,6 +291,7 @@ def _step6_ros_integration(
232
291
  global_dir_name: str,
233
292
  forced_value: bool | None = None,
234
293
  forced_ws: str | None = None,
294
+ sim_version: str | None = None,
235
295
  ) -> tuple[bool, str]:
236
296
  """Prompt for ROS integration and set it up.
237
297
 
@@ -295,6 +355,7 @@ def _step6_ros_integration(
295
355
  ros_res = ros_mgr.setup_ros_workspace(
296
356
  status_callback=ros_status_callback,
297
357
  ws_path=resolved_ws,
358
+ sim_version=sim_version,
298
359
  )
299
360
  except Exception as e:
300
361
  ros_setup_failed = True
@@ -305,7 +366,10 @@ def _step6_ros_integration(
305
366
  raise SystemExit(1)
306
367
 
307
368
  if ros_cloned:
308
- console.print(f" [green]✔[/green] Cloned IsaacSim-ros_workspaces to [dim]{display_path}[/dim]")
369
+ console.print(
370
+ f" [green]✔[/green] Cloned IsaacSim-ros_workspaces "
371
+ f"([bold]{ros_res['ws_ref']}[/bold]) to [dim]{display_path}[/dim]"
372
+ )
309
373
  else:
310
374
  console.print(f" [yellow]✔[/yellow] IsaacSim-ros_workspaces already available in [dim]{display_path}[/dim]")
311
375
 
@@ -404,28 +468,95 @@ def _step7_project_structure(initializer: Initializer):
404
468
  console.print(" [yellow]✔[/yellow] .gitignore already exists. [dim]Kept existing.[/dim]")
405
469
 
406
470
 
407
- def _step8_project_link(initializer: Initializer):
408
- """Symlink managed Isaac Sim to local project."""
471
+ def _step8_project_link(initializer: Initializer, sim_version: str | None = None) -> dict | None:
472
+ """Symlink managed Isaac Sim to local project.
473
+
474
+ Returns the link result, or None when it could not be linked - everything
475
+ after this step describes the linked install, so the caller must stop.
476
+ """
409
477
  console.print("[bold blue][8/10] 🔗 Project Link:[/bold blue] Linking Isaac Sim to project...")
410
- result = initializer.link_managed_isaacsim()
478
+ result = initializer.link_managed_isaacsim(version=sim_version)
411
479
  if result["status"] == "Created":
412
480
  console.print(f" [green]✔[/green] Created symlink: [dim]{result['path']}[/dim]")
481
+ elif result["status"] == "Repointed":
482
+ console.print(
483
+ f" [green]✔[/green] Re-pointed symlink [dim]{result['path']}[/dim] "
484
+ f"to Isaac Sim {sim_version} [dim](was {result['previous']})[/dim]"
485
+ )
413
486
  elif result["status"] == "Existed":
414
487
  console.print(f" [yellow]✔[/yellow] Symlink already exists: [dim]{result['path']}[/dim]")
415
488
  elif result["status"] == "Error":
416
489
  console.print(f" [bold red]❌ Error:[/bold red] {result['message']}")
490
+ return None
491
+ return result
417
492
 
418
493
 
419
- def _step9_vscode_setup(initializer: Initializer):
420
- """Setup VSCode configuration for the project."""
494
+ #: VSCode config outcomes that are not a problem, mapped to how they are marked.
495
+ _VSCODE_OK_STATUSES = {
496
+ "Copied": "green",
497
+ "Copied and patched": "green",
498
+ "Created": "green",
499
+ "Updated": "green",
500
+ "Already up to date": "yellow",
501
+ }
502
+
503
+
504
+ def _step9_vscode_setup(
505
+ initializer: Initializer,
506
+ version: str | None = None,
507
+ version_changed: bool = True,
508
+ ):
509
+ """Setup VSCode configuration for the project.
510
+
511
+ *version_changed* comes from step 8: the Isaac Sim extension paths are
512
+ rewritten only when the project moved to a different version.
513
+ """
421
514
  console.print("[bold blue][9/10] 💻 VSCode Config:[/bold blue] Setting up VSCode configs...")
422
- result = initializer.setup_vscode_configs()
423
- if result["status"] == "Success":
424
- for res in result["results"]:
425
- status_symbol = "[green]✔[/green]" if "patched" in res["status"] or res["status"] == "Copied" else "[yellow]⚠[/yellow]"
426
- console.print(f" {status_symbol} {res['file']}: [dim]{res['status']}[/dim]")
427
- else:
515
+ result = initializer.setup_vscode_configs(version=version, version_changed=version_changed)
516
+
517
+ if result["status"] != "Success":
428
518
  console.print(f" [bold red]❌ Error:[/bold red] {result['message']}")
519
+ return
520
+
521
+ for res in result["results"]:
522
+ status = res["status"]
523
+ colour = _VSCODE_OK_STATUSES.get(status)
524
+ if status == "Error":
525
+ console.print(f" [bold red]❌[/bold red] {res['file']}: [dim]{res.get('message', '')}[/dim]")
526
+ console.print(" Fix it, or delete it and re-run [bold]pow init[/bold].")
527
+ continue
528
+
529
+ symbol = f"[{colour}]✔[/{colour}]" if colour else "[yellow]⚠[/yellow]"
530
+ note = " [dim](kept your other settings)[/dim]" if status == "Updated" else ""
531
+ console.print(f" {symbol} {res['file']}: [dim]{status}[/dim]{note}")
532
+
533
+ for key, (old, new) in (res.get("changed") or {}).items():
534
+ old_text = "[dim]unset[/dim]" if old is None else _json_value(old)
535
+ new_text = "[dim]removed[/dim]" if new is None else f"[bold]{_json_value(new)}[/bold]"
536
+ console.print(f" [dim]{key}:[/dim] {old_text} → {new_text}", highlight=False)
537
+
538
+ if res["file"] == "settings.json" and not version_changed:
539
+ console.print(
540
+ f" [dim]python.analysis.extraPaths: kept "
541
+ f"(Isaac Sim {result['version']} unchanged)[/dim]"
542
+ )
543
+
544
+ if res.get("warning"):
545
+ console.print(f" [yellow]⚠[/yellow] [dim]{res['warning']}[/dim]")
546
+
547
+
548
+ def _json_value(value) -> str:
549
+ """Render a settings value the way settings.json spells it, shortening containers."""
550
+ if isinstance(value, (list, dict)) and len(value) > 3:
551
+ return f"[dim]{len(value)} entries[/dim]"
552
+ return json.dumps(value)
553
+
554
+
555
+ def _toml_value(value) -> str:
556
+ """Render a setting the way pow.toml spells it, so diffs read like the file."""
557
+ if isinstance(value, bool):
558
+ return "true" if value else "false"
559
+ return str(value)
429
560
 
430
561
 
431
562
  def _step10_finalize(
@@ -433,14 +564,25 @@ def _step10_finalize(
433
564
  override_pow_toml: bool,
434
565
  ros_enabled: bool,
435
566
  isaacsim_ros_ws: str = "~/IsaacSim-ros_workspaces",
436
- ):
437
- """Generate pow.toml configuration."""
438
-
439
-
440
- console.print("[bold blue][10/10] ✅ Finalizing:[/bold blue] Generating configuration...")
441
-
442
- # Ensure user-home alias in Omniverse config
567
+ sim_version: str | None = None,
568
+ ) -> bool:
569
+ """Generate pow.toml configuration. Returns False when it could not be written."""
570
+ # Both calls return their outcome instead of printing, so the step header can
571
+ # be rendered afterwards and carry the right icon.
443
572
  alias_result = initializer.setup_omniverse_user_home_alias()
573
+ result = initializer.create_pow_toml(
574
+ override=override_pow_toml,
575
+ enable_ros=ros_enabled,
576
+ isaacsim_ros_ws=isaacsim_ros_ws,
577
+ sim_version=sim_version,
578
+ )
579
+ failed = result["status"] == "Error"
580
+
581
+ icon = "❌" if failed else "✅"
582
+ console.print(
583
+ f"[bold blue][10/10] {icon} Finalizing:[/bold blue] Generating configuration..."
584
+ )
585
+
444
586
  if alias_result["status"] == "created":
445
587
  console.print(f" [green]✔[/green] Added user-home alias in [dim]{alias_result['path']}[/dim]")
446
588
  elif alias_result["status"] == "updated":
@@ -448,25 +590,52 @@ def _step10_finalize(
448
590
  else:
449
591
  console.print(f" [yellow]✔[/yellow] user-home alias already set in [dim]{alias_result['path']}[/dim]")
450
592
 
451
- # Create pow.toml
452
- result = initializer.create_pow_toml(
453
- override=override_pow_toml,
454
- enable_ros=ros_enabled,
455
- isaacsim_ros_ws=isaacsim_ros_ws,
456
- )
457
593
  if result["status"] == "Created":
458
594
  console.print(" [green]✔[/green] Created pow.toml (from template)")
595
+ elif result["status"] == "Updated":
596
+ changed = result.get("changed") or {}
597
+ if not changed:
598
+ console.print(" [yellow]✔[/yellow] pow.toml already up to date")
599
+ else:
600
+ console.print(
601
+ " [green]✔[/green] Updated pow.toml "
602
+ "[dim](kept your other settings)[/dim]"
603
+ )
604
+ for key, (old, new) in changed.items():
605
+ old_text = "[dim]unset[/dim]" if old is None else _toml_value(old)
606
+ console.print(
607
+ f" [dim]{key}:[/dim] {old_text} → [bold]{_toml_value(new)}[/bold]",
608
+ highlight=False,
609
+ )
459
610
  elif result["status"] == "Existed":
460
611
  console.print(" [yellow]✔[/yellow] Kept existing pow.toml")
612
+ elif failed:
613
+ console.print(" [bold red]❌ Error:[/bold red] pow.toml could not be parsed")
614
+ console.print(f" [dim]{result['message']}[/dim]")
615
+ console.print(" Fix it, or delete it and re-run [bold]pow init[/bold].")
461
616
  else:
462
617
  console.print(" [yellow]⚠[/yellow] pow.toml template not found. [dim]Skipped.[/dim]")
463
618
 
619
+ return not failed
620
+
464
621
 
465
622
  # ── Command entry point ───────────────────────────────────────────────────────
466
623
 
467
624
  @click.command(name="init")
468
- def init_cmd():
469
- """Initialize Isaac Sim project."""
625
+ @click.option(
626
+ "--sim-version",
627
+ "sim_version",
628
+ type=click.Choice(PowConfig.SUPPORTED_ISAACSIM_VERSIONS),
629
+ default=None,
630
+ help="Isaac Sim version to install. Skips the interactive prompt.",
631
+ )
632
+ def init_cmd(sim_version: str | None):
633
+ """Initialize Isaac Sim project.
634
+
635
+ \b
636
+ 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.
638
+ """
470
639
  initializer = Initializer()
471
640
  config = initializer.get_config_path()
472
641
  global_dir_name = config["global_dir_name"]
@@ -485,30 +654,67 @@ def init_cmd():
485
654
  override_pow_toml = _step2_check_existing_config(initializer)
486
655
  _step3_global_folder(initializer, global_path)
487
656
 
488
- download_result = _step4_download_isaacsim(initializer)
489
- if download_result is None:
490
- return
491
-
492
- _step5_optimization(initializer, download_result["path"])
493
-
494
- # Use existing ROS setting if not overriding pow.toml
657
+ # Reuse settings from an existing pow.toml the user chose to keep.
495
658
  ros_forced = None
496
659
  ros_ws_forced = None
660
+ version_forced = None
497
661
  if not override_pow_toml:
498
662
  try:
499
663
  ros_forced = initializer.config.get("enable_ros", False)
500
664
  ros_ws_forced = initializer.config.get("isaacsim_ros_ws", None)
665
+ version_forced = initializer.config.get("version", None)
501
666
  except Exception:
502
667
  pass
503
668
 
669
+ download_result = _step4_download_isaacsim(
670
+ initializer, flag_version=sim_version, config_version=version_forced,
671
+ )
672
+ if download_result is None:
673
+ return
674
+
675
+ resolved_version = download_result["version"]
676
+
677
+ _step5_optimization(initializer, download_result["path"])
678
+
504
679
  ros_enabled, isaacsim_ros_ws = _step6_ros_integration(
505
- initializer, global_dir_name, forced_value=ros_forced, forced_ws=ros_ws_forced,
680
+ initializer, global_dir_name,
681
+ forced_value=ros_forced, forced_ws=ros_ws_forced,
682
+ sim_version=resolved_version,
506
683
  )
507
684
  _step7_project_structure(initializer)
508
- _step8_project_link(initializer)
509
- _step9_vscode_setup(initializer)
685
+ linked = _step8_project_link(initializer, sim_version=resolved_version)
686
+ if linked is None:
687
+ # Everything below describes the linked install: the .vscode configs are
688
+ # read from it and pow.toml records its version. Writing either against
689
+ # a link that is not there would leave the project inconsistent.
690
+ console.print(
691
+ Panel(
692
+ "[bold red]✘ Initialization incomplete: Isaac Sim could not be linked."
693
+ "[/bold red]",
694
+ border_style="red",
695
+ )
696
+ )
697
+ raise SystemExit(1)
510
698
 
511
- _step10_finalize(initializer, override_pow_toml, ros_enabled, isaacsim_ros_ws)
699
+ _step9_vscode_setup(
700
+ initializer,
701
+ version=resolved_version,
702
+ version_changed=linked["status"] in ("Created", "Repointed"),
703
+ )
704
+
705
+ finalized = _step10_finalize(
706
+ initializer, override_pow_toml, ros_enabled, isaacsim_ros_ws,
707
+ sim_version=resolved_version,
708
+ )
709
+
710
+ if not finalized:
711
+ console.print(
712
+ Panel(
713
+ "[bold red]✘ Initialization incomplete: pow.toml was not updated.[/bold red]",
714
+ border_style="red",
715
+ )
716
+ )
717
+ raise SystemExit(1)
512
718
 
513
719
  console.print(
514
720
  Panel(
@@ -144,7 +144,7 @@ def fix_cmd(path, short):
144
144
  if not issues:
145
145
  continue
146
146
 
147
- fix_file(usda_file, issues)
147
+ fix_file(usda_file, issues, alias_config)
148
148
  files_fixed += 1
149
149
  total_fixed += len(issues)
150
150
 
@@ -190,15 +190,9 @@ def _print_issue(issue, *, short: bool = False) -> None:
190
190
  """Print a single lint issue."""
191
191
  rel_path = _short_path(issue.file)
192
192
  if short:
193
- if "simready_content" in issue.original:
194
- problem = "relative path → use sim-ready staging S3 URL"
195
- elif "Pow" in issue.original:
196
- problem = "relative path → use pow-assets alias"
197
- else:
198
- problem = "relative path → use NVIDIA production S3 URL"
199
193
  console.print(
200
194
  f" [dim]{rel_path}[/dim]:[green]{issue.line}[/green]"
201
- f" [red]●[/red] {problem}",
195
+ f" [red]●[/red] {escape(issue.label)}",
202
196
  soft_wrap=True,
203
197
  )
204
198
  else:
@@ -223,7 +217,7 @@ def _print_fix(issue, *, short: bool = False) -> None:
223
217
  if short:
224
218
  console.print(
225
219
  f" [dim]{rel_path}[/dim]:[green]{issue.line}[/green]"
226
- f" [green]✔[/green] fixed relative path",
220
+ f" [green]✔[/green] fixed: {escape(issue.label)}",
227
221
  soft_wrap=True,
228
222
  )
229
223
  else:
@@ -4,10 +4,9 @@ import click
4
4
 
5
5
  from .init import init_cmd
6
6
  # from .add import add_group
7
- from .check import check_cmd
8
7
  from .lint import lint_group
9
8
  from .run import run_cmd
10
- from .sim import sim_cmd
9
+ from .sim import sim_group
11
10
  from .ros import ros_group
12
11
  from .python import python_cmd
13
12
  from .asset import asset_group
@@ -30,10 +29,9 @@ def pow_group():
30
29
 
31
30
  # Register commands
32
31
  pow_group.add_command(init_cmd)
33
- pow_group.add_command(check_cmd)
34
32
  pow_group.add_command(lint_group)
35
33
  pow_group.add_command(run_cmd)
36
- pow_group.add_command(sim_cmd)
34
+ pow_group.add_command(sim_group)
37
35
  pow_group.add_command(ros_group)
38
36
  pow_group.add_command(python_cmd)
39
37
  pow_group.add_command(asset_group)