sima-cli 2.1.16__tar.gz → 2.1.17__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 (194) hide show
  1. {sima_cli-2.1.16/sima_cli.egg-info → sima_cli-2.1.17}/PKG-INFO +4 -1
  2. {sima_cli-2.1.16 → sima_cli-2.1.17}/README.md +2 -0
  3. {sima_cli-2.1.16 → sima_cli-2.1.17}/pyproject.toml +2 -1
  4. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/__version__.py +1 -1
  5. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/cli.py +150 -49
  6. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/download/downloader.py +2 -0
  7. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/metadata_installer.py +28 -11
  8. sima_cli-2.1.17/sima_cli/models/__init__.py +3 -0
  9. sima_cli-2.1.17/sima_cli/models/client.py +372 -0
  10. sima_cli-2.1.17/sima_cli/models/commands.py +223 -0
  11. sima_cli-2.1.17/sima_cli/models/rendering.py +473 -0
  12. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/commands.py +66 -25
  13. sima_cli-2.1.17/sima_cli/sdk/docker_staging.py +22 -0
  14. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/install.py +108 -13
  15. sima_cli-2.1.17/sima_cli/sdk/model_compiler.py +140 -0
  16. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/neat.py +32 -0
  17. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/utils.py +443 -129
  18. sima_cli-2.1.17/sima_cli/sdk/vscode_extensions.py +197 -0
  19. sima_cli-2.1.17/sima_cli/update/ab_state.py +91 -0
  20. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/bootimg.py +7 -2
  21. sima_cli-2.1.17/sima_cli/update/emmc.py +68 -0
  22. sima_cli-2.1.17/sima_cli/update/fast_upload.py +79 -0
  23. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/netboot.py +191 -43
  24. sima_cli-2.1.17/sima_cli/update/netboot_artifacts.py +105 -0
  25. sima_cli-2.1.17/sima_cli/update/netboot_device.py +177 -0
  26. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/query.py +110 -22
  27. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/remote.py +52 -32
  28. sima_cli-2.1.17/sima_cli/update/rootfs.py +96 -0
  29. sima_cli-2.1.17/sima_cli/update/staging.py +76 -0
  30. sima_cli-2.1.17/sima_cli/update/swu.py +462 -0
  31. sima_cli-2.1.17/sima_cli/update/swu_artifacts.py +286 -0
  32. sima_cli-2.1.17/sima_cli/update/swu_certificate.py +57 -0
  33. sima_cli-2.1.17/sima_cli/update/swu_target.py +85 -0
  34. sima_cli-2.1.17/sima_cli/update/uboot_environment.py +78 -0
  35. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/updater.py +70 -18
  36. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/pkg_update_check.py +3 -1
  37. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/vulcan/artifacts.py +1 -0
  38. {sima_cli-2.1.16 → sima_cli-2.1.17/sima_cli.egg-info}/PKG-INFO +4 -1
  39. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli.egg-info/SOURCES.txt +43 -0
  40. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli.egg-info/requires.txt +1 -0
  41. sima_cli-2.1.17/tests/unit/test_ab_overlay.py +125 -0
  42. sima_cli-2.1.17/tests/unit/test_bootimg_defaults.py +37 -0
  43. sima_cli-2.1.17/tests/unit/test_bootimg_recovery.py +118 -0
  44. sima_cli-2.1.17/tests/unit/test_emmc.py +92 -0
  45. sima_cli-2.1.17/tests/unit/test_fast_upload.py +49 -0
  46. sima_cli-2.1.17/tests/unit/test_firmware_creation_order.py +65 -0
  47. sima_cli-2.1.17/tests/unit/test_firmware_layout.py +66 -0
  48. sima_cli-2.1.17/tests/unit/test_firmware_selector.py +64 -0
  49. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_install_stub.py +69 -4
  50. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_metadata_installer.py +137 -13
  51. sima_cli-2.1.17/tests/unit/test_models.py +632 -0
  52. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_netboot.py +8 -1
  53. sima_cli-2.1.17/tests/unit/test_netboot_autoflash.py +53 -0
  54. sima_cli-2.1.17/tests/unit/test_netboot_device.py +292 -0
  55. sima_cli-2.1.17/tests/unit/test_netboot_ip_recovery.py +129 -0
  56. sima_cli-2.1.17/tests/unit/test_netboot_mirror.py +186 -0
  57. sima_cli-2.1.17/tests/unit/test_netboot_trootctl.py +95 -0
  58. sima_cli-2.1.17/tests/unit/test_palette_image_discovery.py +78 -0
  59. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_pkg_update_check.py +25 -0
  60. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_sdk_image_detection.py +840 -56
  61. sima_cli-2.1.17/tests/unit/test_sdk_model_compiler.py +282 -0
  62. sima_cli-2.1.17/tests/unit/test_swu.py +573 -0
  63. sima_cli-2.1.17/tests/unit/test_swu_certificate.py +208 -0
  64. sima_cli-2.1.17/tests/unit/test_swu_errors.py +162 -0
  65. sima_cli-2.1.17/tests/unit/test_swu_mirror.py +331 -0
  66. sima_cli-2.1.17/tests/unit/test_swu_target.py +58 -0
  67. sima_cli-2.1.17/tests/unit/test_uboot_environment.py +67 -0
  68. sima_cli-2.1.17/tests/unit/test_update_staging.py +111 -0
  69. sima_cli-2.1.17/tests/unit/test_vscode_extension_pins.py +362 -0
  70. {sima_cli-2.1.16 → sima_cli-2.1.17}/LICENSE +0 -0
  71. {sima_cli-2.1.16 → sima_cli-2.1.17}/MANIFEST.in +0 -0
  72. {sima_cli-2.1.16 → sima_cli-2.1.17}/requirements.txt +0 -0
  73. {sima_cli-2.1.16 → sima_cli-2.1.17}/setup.cfg +0 -0
  74. {sima_cli-2.1.16 → sima_cli-2.1.17}/setup.py +0 -0
  75. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/__init__.py +0 -0
  76. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/__main__.py +0 -0
  77. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/app_zoo/__init__.py +0 -0
  78. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/app_zoo/app.py +0 -0
  79. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/app_zoo/commands.py +0 -0
  80. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/auth/__init__.py +0 -0
  81. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/auth/auth0.py +0 -0
  82. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/auth/devportal.py +0 -0
  83. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/auth/login.py +0 -0
  84. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/data/__init__.py +0 -0
  85. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/data/resources_internal.yaml +0 -0
  86. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/data/resources_public.yaml +0 -0
  87. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/deploy_only/__init__.py +0 -0
  88. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/deploy_only/device/__init__.py +0 -0
  89. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/deploy_only/device/commands.py +0 -0
  90. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  91. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/deploy_only/mpk/commands.py +0 -0
  92. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/discover/__init__.py +0 -0
  93. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/discover/discover.py +0 -0
  94. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/discover/linuxll.py +0 -0
  95. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/download/__init__.py +0 -0
  96. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/__init__.py +0 -0
  97. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/compatibility.py +0 -0
  98. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/github_assets.py +0 -0
  99. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/hostdriver.py +0 -0
  100. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/metadata_info.py +0 -0
  101. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/metadata_validator.py +0 -0
  102. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/optiview.py +0 -0
  103. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/package_builder.py +0 -0
  104. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/palette.py +0 -0
  105. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/install/registry.py +0 -0
  106. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/mla/__init__.py +0 -0
  107. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/mla/meminfo.py +0 -0
  108. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/model_zoo/__init__.py +0 -0
  109. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/model_zoo/model.py +0 -0
  110. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/network/__init__.py +0 -0
  111. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/network/network.py +0 -0
  112. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/playbooks/__init__.py +0 -0
  113. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/playbooks/commands.py +0 -0
  114. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/playbooks/manager.py +0 -0
  115. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/__init__.py +0 -0
  116. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/cmdexec.py +0 -0
  117. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/config.py +0 -0
  118. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/linux_devkit_network.py +0 -0
  119. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/linux_shared_network.py +0 -0
  120. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/network_doctor.py +0 -0
  121. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/preinstall.py +0 -0
  122. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/requirements.json +0 -0
  123. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/script.py +0 -0
  124. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/stop.py +0 -0
  125. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/sdk/uninstall.py +0 -0
  126. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/serial/__init__.py +0 -0
  127. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/serial/serial.py +0 -0
  128. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/storage/__init__.py +0 -0
  129. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/storage/nvme.py +0 -0
  130. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/storage/sdcard.py +0 -0
  131. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/__init__.py +0 -0
  132. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/bmaptool.py +0 -0
  133. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/cleanlog.py +0 -0
  134. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/elxr.py +0 -0
  135. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/update/local.py +0 -0
  136. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/upgrade/__init__.py +0 -0
  137. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/upgrade/selfupdate.py +0 -0
  138. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/__init__.py +0 -0
  139. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/api_common.py +0 -0
  140. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/artifactory.py +0 -0
  141. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/common.py +0 -0
  142. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/config.py +0 -0
  143. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/config_loader.py +0 -0
  144. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/container_registries.py +0 -0
  145. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/deprecation.py +0 -0
  146. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/device_api.py +0 -0
  147. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/disk.py +0 -0
  148. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/docker.py +0 -0
  149. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/env.py +0 -0
  150. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/errors.py +0 -0
  151. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/mpk_api.py +0 -0
  152. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/net.py +0 -0
  153. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/network.py +0 -0
  154. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/pcie.py +0 -0
  155. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/serializers.py +0 -0
  156. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/services.py +0 -0
  157. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/utils/tag.py +0 -0
  158. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/vulcan/__init__.py +0 -0
  159. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli/vulcan/commands.py +0 -0
  160. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli.egg-info/dependency_links.txt +0 -0
  161. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli.egg-info/entry_points.txt +0 -0
  162. {sima_cli-2.1.16 → sima_cli-2.1.17}/sima_cli.egg-info/top_level.txt +0 -0
  163. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/__init__.py +0 -0
  164. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/e2e/__init__.py +0 -0
  165. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/__init__.py +0 -0
  166. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_app_zoo.py +0 -0
  167. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_appzoo_commands.py +0 -0
  168. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_auth.py +0 -0
  169. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_auth0.py +0 -0
  170. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_cli.py +0 -0
  171. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_cli_stdio.py +0 -0
  172. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_cli_update.py +0 -0
  173. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_cli_update_rerun.py +0 -0
  174. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_container_registries.py +0 -0
  175. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_deprecation.py +0 -0
  176. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_docker_utils.py +0 -0
  177. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_download.py +0 -0
  178. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_elxr_update.py +0 -0
  179. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_firmware.py +0 -0
  180. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_generate_cli_markdown_docs.py +0 -0
  181. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_metadata_validator.py +0 -0
  182. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_model_zoo.py +0 -0
  183. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_modelzoo_commands.py +0 -0
  184. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_package_builder.py +0 -0
  185. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_pcie.py +0 -0
  186. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_sdk_linux_shared_network.py +0 -0
  187. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_sdk_network_doctor.py +0 -0
  188. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_sdk_preinstall.py +0 -0
  189. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_sdk_uninstall.py +0 -0
  190. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_selfupdate.py +0 -0
  191. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_skills_commands.py +0 -0
  192. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_skills_manager.py +0 -0
  193. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_utils.py +0 -0
  194. {sima_cli-2.1.16 → sima_cli-2.1.17}/tests/unit/test_vulcan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.16
3
+ Version: 2.1.17
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://community.sima.ai/
6
6
  Author: SiMa.ai
@@ -21,6 +21,7 @@ Requires-Dist: requests<3.0,>=2.32.4
21
21
  Requires-Dist: tqdm>=4.64
22
22
  Requires-Dist: pyyaml>=6.0
23
23
  Requires-Dist: paramiko>=3.5.1
24
+ Requires-Dist: cryptography>=3.3
24
25
  Requires-Dist: plotext==5.3.2
25
26
  Requires-Dist: rich==14.0.0
26
27
  Requires-Dist: InquirerPy==0.3.4
@@ -34,6 +35,8 @@ Dynamic: requires-python
34
35
 
35
36
  # sima-cli - SiMa Developer Portal CLI Tool
36
37
 
38
+ **Documentation:** [English](docs/sima-cli/index.md) | [한국어](docs/i18n/ko/sima-cli/index.md) | [日本語](docs/i18n/ja/sima-cli/index.md) | [繁體中文](docs/i18n/zh-Hant/sima-cli/index.md) | [Українська](docs/i18n/uk/sima-cli/index.md)
39
+
37
40
  [![Python 3.8](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.8&label=python%203.8)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
38
41
  [![Python 3.9](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.9&label=python%203.9)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
39
42
  [![Python 3.10](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.10&label=python%203.10)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
@@ -1,5 +1,7 @@
1
1
  # sima-cli - SiMa Developer Portal CLI Tool
2
2
 
3
+ **Documentation:** [English](docs/sima-cli/index.md) | [한국어](docs/i18n/ko/sima-cli/index.md) | [日本語](docs/i18n/ja/sima-cli/index.md) | [繁體中文](docs/i18n/zh-Hant/sima-cli/index.md) | [Українська](docs/i18n/uk/sima-cli/index.md)
4
+
3
5
  [![Python 3.8](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.8&label=python%203.8)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
4
6
  [![Python 3.9](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.9&label=python%203.9)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
5
7
  [![Python 3.10](https://img.shields.io/github/actions/workflow/status/sima-neat/sima-cli/vulcan-ci.yml?branch=main&job=Compatibility%20Python%203.10&label=python%203.10)](https://github.com/sima-neat/sima-cli/actions/workflows/vulcan-ci.yml)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.16"
7
+ version = "2.1.17"
8
8
  description = "CLI tool for SiMa Developer Portal to download models, firmware, and apps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "tqdm>=4.64",
28
28
  "pyyaml>=6.0",
29
29
  "paramiko>=3.5.1",
30
+ "cryptography>=3.3",
30
31
  "plotext==5.3.2",
31
32
  "rich==14.0.0",
32
33
  "InquirerPy==0.3.4",
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.16"
2
+ __version__ = "2.1.17"
@@ -1,12 +1,14 @@
1
1
  import os
2
2
  import shutil
3
3
  import sys
4
+ from contextlib import nullcontext, redirect_stdout
4
5
  import click
5
6
  from rich.console import Console
6
7
  from rich.panel import Panel
7
8
  from rich.text import Text
8
9
  from sima_cli.utils.env import get_environment_type
9
10
  from sima_cli.update.updater import perform_update
11
+ from sima_cli.update.swu import handle_update
10
12
  from sima_cli.model_zoo.model import list_models, download_model, describe_model
11
13
  from sima_cli.app_zoo.app import list_apps, download_app, describe_app
12
14
  from sima_cli.utils.config_loader import internal_resource_exists
@@ -37,6 +39,7 @@ from sima_cli.install.registry import register_packages_commands
37
39
  from sima_cli.upgrade.selfupdate import register_selfupdate_command
38
40
  from sima_cli.playbooks import register_playbook_commands
39
41
  from sima_cli.vulcan import register_vulcan_commands
42
+ from sima_cli.models import register_models_commands
40
43
  from sima_cli.vulcan.commands import (
41
44
  ENV_METAVAR,
42
45
  _environment_shortcut_options,
@@ -99,19 +102,7 @@ def _rerun_current_command() -> None:
99
102
  os.execvpe(sys.executable, cmd, env)
100
103
 
101
104
 
102
- # Entry point for the CLI tool using Click's command group decorator
103
- @click.group(context_settings=dict(help_option_names=["-h", "--help", "-?"], max_content_width=120))
104
- @click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources, Authorized Sima employees only")
105
- @click.option('-y', '--yes', is_flag=True, help="Assume yes for confirmation prompts.")
106
- @click.version_option(version=f"{__version__}", message="SiMa CLI version: %(version)s")
107
- @click.pass_context
108
- def main(ctx, internal, yes):
109
- """
110
- sima-cli – SiMa Developer Portal CLI Tool
111
-
112
- Global Options:
113
- --internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
114
- """
105
+ def _initialize_main_context(ctx, internal, yes):
115
106
  _configure_stdio_errors()
116
107
  auto_accept_update = yes or _update_auto_confirm_requested(sys.argv)
117
108
  previous_auto_accept = os.environ.get("SIMA_CLI_AUTO_ACCEPT_UPDATE")
@@ -134,26 +125,26 @@ def main(ctx, internal, yes):
134
125
  if not internal:
135
126
  internal = os.getenv("SIMA_CLI_INTERNAL", "0") in ("1", "true", "yes")
136
127
 
137
- if internal and not internal_resource_exists():
138
- click.echo("❌ You have specified -i or --internal argument to access internal resources, but you do not have an internal resource map configured.")
128
+ defer_artifactory_access = ctx.invoked_subcommand == "update" or ctx.meta.get("daily_netboot", False)
129
+ if internal and not internal_resource_exists() and not defer_artifactory_access:
130
+ click.echo("❌ You have specified -i or --internal argument to access internal resources, but you do not have an internal resource map configured.")
139
131
  click.echo("Refer to the confluence page to find out how to configure internal resource map.")
140
- exit(0)
132
+ exit(0)
141
133
 
142
134
  internal_reachable = True
143
135
  if internal:
144
136
  internal_reachable = check_artifactory_reachability()
145
137
 
146
- if internal and not internal_reachable and not _allows_external_prerelease_fallback(sys.argv):
138
+ if internal and not internal_reachable and not defer_artifactory_access and not _allows_external_prerelease_fallback(sys.argv):
147
139
  click.secho("❌ You have specified -i or --internal argument to access internal resources, but you can't connect to Artifactory.", fg='red')
148
140
  click.secho("Please make sure you are connected to VPN or are on the corporate network.", fg='red')
149
141
  exit(0)
150
142
 
151
- if internal and not internal_reachable:
143
+ if internal and not internal_reachable and not defer_artifactory_access:
152
144
  click.secho(
153
145
  "⚠️ Internal resources are unreachable. --force allows this update to use the external pre-release mirror.",
154
146
  fg="yellow",
155
147
  )
156
-
157
148
 
158
149
  ctx.obj["internal"] = internal
159
150
  ctx.obj["internal_reachable"] = internal_reachable
@@ -167,12 +158,61 @@ def main(ctx, internal, yes):
167
158
  click.echo(f"🔧 Environment: {env_type} ({env_subtype})")
168
159
 
169
160
 
161
+ class InspectionAwareGroup(click.Group):
162
+ def parse_args(self, ctx, args):
163
+ # Capture the actual Click arguments (also works with CliRunner), before
164
+ # the root callback can perform a CLI self-update or network login.
165
+ options = args[:args.index('--')] if '--' in args else args
166
+ ctx.meta['daily_netboot'] = (
167
+ _command_name_from_argv(['sima-cli'] + options) == 'bootimg'
168
+ and any(arg in ('--netboot', '-n', '--autoflash', '-a') for arg in options)
169
+ )
170
+ if _command_name_from_argv(['sima-cli'] + options) == 'update' and '--inspect' in options:
171
+ ctx.meta['update_inspect'] = True
172
+ return super().parse_args(ctx, args)
173
+
174
+
175
+ # Entry point for the CLI tool using Click's command group decorator
176
+ @click.group(cls=InspectionAwareGroup, context_settings=dict(help_option_names=["-h", "--help", "-?"], max_content_width=120))
177
+ @click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources, Authorized Sima employees only")
178
+ @click.option('-y', '--yes', is_flag=True, help="Assume yes for confirmation prompts.")
179
+ @click.version_option(version=f"{__version__}", message="SiMa CLI version: %(version)s")
180
+ @click.pass_context
181
+ def main(ctx, internal, yes):
182
+ """
183
+ sima-cli – SiMa Developer Portal CLI Tool
184
+
185
+ Global Options:
186
+ --internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
187
+ """
188
+ internal = internal or os.getenv("SIMA_CLI_INTERNAL", "0") in ("1", "true", "yes")
189
+ if internal:
190
+ Console(stderr=True).print(Panel(
191
+ "Pre-release software may be unstable. Use at your own risk.",
192
+ title="Pre-release software", border_style="yellow",
193
+ ))
194
+ if ctx.meta.get('update_inspect'):
195
+ ctx.ensure_object(dict)
196
+ ctx.obj.update(internal=internal, yes=yes)
197
+ return
198
+ # Model Registry JSON must remain parseable on stdout. Route root-level
199
+ # update and environment diagnostics to stderr for this command group.
200
+ output_context = (
201
+ redirect_stdout(sys.stderr)
202
+ if ctx.invoked_subcommand == "models"
203
+ else nullcontext()
204
+ )
205
+ with output_context:
206
+ _initialize_main_context(ctx, internal, yes)
207
+
208
+
170
209
  # ----------------------
171
210
  # SDK Command
172
211
  # ----------------------
173
212
  register_sdk_commands(main)
174
213
  register_playbook_commands(main)
175
214
  register_vulcan_commands(main)
215
+ register_models_commands(main)
176
216
 
177
217
 
178
218
  # ----------------------
@@ -294,7 +334,8 @@ def download(ctx, url, dest):
294
334
  is_flag=True,
295
335
  help=(
296
336
  "If the internal mirror is unreachable, fall back to the external pre-release mirror "
297
- "without signature verification; without --internal, select that mirror directly (ELXR only)."
337
+ "(eLxr only). eLxr 3.0+ still verifies signed full-system bundles. "
338
+ "On eLxr 2.1, this disables repository signature verification and, without --internal, selects the mirror directly."
298
339
  )
299
340
  )
300
341
  @click.option(
@@ -307,10 +348,13 @@ def download(ctx, url, dest):
307
348
  "--dryrun",
308
349
  is_flag=True,
309
350
  default=False,
310
- help="For ELXR updates only, validate the update path and print the simaai-ota command without running it."
351
+ help="For eLxr updates, validate the update path and show the command without installing."
311
352
  )
353
+ @click.option("--inspect", "inspect_state", is_flag=True, help="Show eLxr 3.0+ A/B slot state without updating (local or --ip).")
354
+ @click.option("--signing-cert", metavar="URL_OR_FILE", help="SWUpdate PEM verification certificate: HTTP(S) URL or local file. Defaults to the certificate for the selected channel when configured (eLxr 3.0+).")
355
+ @click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation; verify remote boot health.")
312
356
  @click.pass_context
313
- def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun):
357
+ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun, inspect_state, reboot, signing_cert):
314
358
  """
315
359
  Update the software on a SiMa DevKit or remote SiMa device.
316
360
 
@@ -319,6 +363,24 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
319
363
  or remote devices accessible over the network). Updates may be
320
364
  installed directly on the device or pushed from a development host.
321
365
 
366
+ eLxr 3.0+ uses signed full-system SWU bundles. Use --inspect for A/B
367
+ state, --ip for remote updates, and --reboot to reboot after installation.
368
+ The verification certificate is downloaded for the selected channel when configured; use
369
+ --signing-cert URL_OR_FILE for an image signed with your own certificate.
370
+ Developer-portal version lookup for 3.0 is not available yet.
371
+
372
+ Internal mode warns that pre-release software may be unstable and is used
373
+ at your own risk. On eLxr 3.0+, unavailable Artifactory access falls back
374
+ to the public daily platform mirror, including missing login, HTTP 401/403,
375
+ connection failures, timeouts, and server errors. Exact build names select
376
+ directly; partial matches show builds newest first by build number.
377
+
378
+ Mirror downloads verify the indexed size and SHA-256 before signed
379
+ installation. A missing build or failed download stops before installation.
380
+ A connection loss after installation starts requires inspecting the board
381
+ before retrying. Devices running eLxr 2.1.3 retain the APT update flow;
382
+ upgrading those devices to 3.0 requires recovery/provisioning first.
383
+
322
384
  How Version Resolution Works:
323
385
 
324
386
  • If a version string is provided (e.g., ``1.7.0``), sima-cli automatically resolves it to the correct downloadable firmware asset based on channel, flavor, and board type.
@@ -368,7 +430,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
368
430
 
369
431
  sima-cli update -v 1.7.0 -y
370
432
 
371
- # Update ELXR to the latest official release without prompts
433
+ # Update legacy eLxr (<3.0) to the latest official release without prompts
372
434
 
373
435
  sima-cli update -y
374
436
 
@@ -376,11 +438,11 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
376
438
 
377
439
  sima-cli -i update -y
378
440
 
379
- # Update ELXR from the public pre-release mirror without prompts
441
+ # Update legacy eLxr (<3.0) from the public pre-release mirror without prompts
380
442
 
381
443
  sima-cli -y update -f -y
382
444
 
383
- # Validate ELXR update path without running simaai-ota
445
+ # Validate the eLxr update path without installing
384
446
 
385
447
  sima-cli update --dryrun
386
448
 
@@ -392,6 +454,29 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
392
454
  # Prioritize explicit --version option over positional argument
393
455
  version_or_url = version_option or version_or_url
394
456
  is_elxr = is_devkit_running_elxr()
457
+ if inspect_state and (version_or_url or dryrun or force or troot_only or reboot or signing_cert is not None or flavor != 'auto'):
458
+ raise click.UsageError("--inspect cannot be combined with installation options.")
459
+ try:
460
+ if handle_update(version_or_url, ip=ip, passwd=passwd,
461
+ internal=ctx.obj.get("internal", False),
462
+ auto_confirm=yes or ctx.obj.get("yes", False), dryrun=dryrun,
463
+ signing_cert=signing_cert, reboot=reboot, inspect=inspect_state, force=force,
464
+ troot_only=troot_only, flavor=flavor, local_elxr=is_elxr):
465
+ return
466
+ except (click.ClickException, click.Abort):
467
+ raise
468
+ except Exception as exc:
469
+ raise click.ClickException(str(exc)) from exc
470
+
471
+ # Legacy updates retain their original access/--force policy after dispatch.
472
+ if ctx.obj.get("internal", False):
473
+ if not internal_resource_exists():
474
+ raise click.ClickException("Internal resources are not configured on this machine.")
475
+ if not ctx.obj.get("internal_reachable", True) and not force:
476
+ raise click.ClickException(
477
+ "Artifactory is unreachable. Connect to VPN or the corporate network. "
478
+ "Automatic daily SWU fallback requires a device already running eLxr 3.0+."
479
+ )
395
480
 
396
481
  if dryrun and not is_elxr:
397
482
  raise click.ClickException("--dryrun is only supported when running update on an ELXR devkit.")
@@ -514,14 +599,16 @@ def show_mla_memory_usage(ctx):
514
599
  # ----------------------
515
600
  @main.command(name="bootimg")
516
601
  @click.option("-v", "--version", required=True, help="Firmware version to download and write (e.g., 1.6.0)")
517
- @click.option("-b", "--boardtype", type=click.Choice(["modalix", "mlsoc"], case_sensitive=False), default="mlsoc", show_default=True, help="Target board type.")
518
- @click.option("-t", "--fwtype", type=click.Choice(["yocto", "elxr"], case_sensitive=False), default="yocto", show_default=True, help="Target firmware type.")
602
+ @click.option("-b", "--boardtype", type=click.Choice(["modalix", "mlsoc"], case_sensitive=False), default="modalix", show_default=True, help="Target board type.")
603
+ @click.option("-t", "--fwtype", type=click.Choice(["yocto", "elxr"], case_sensitive=False), default="elxr", show_default=True, help="Target firmware type.")
519
604
  @click.option("-n", "--netboot", is_flag=True, default=False, show_default=True, help="Prepare image for network boot and launch TFTP server.")
520
- @click.option("--devkit-ip", required=False, help="Optional DevKit IP address for pre-netboot version probing.")
605
+ @click.option("-f", "--force", is_flag=True, help="Allow daily mirror fallback if Artifactory is unavailable (internal Modalix eLxr netboot only).")
606
+ @click.option("--recovery", is_flag=True, help="Write eLxr Modalix recovery media for automatic eMMC recovery.")
607
+ @click.option("--devkit", "--devkit-ip", "devkit_ip", required=False, help="DevKit IP for remote netboot; discover and select a DevKit when omitted.")
521
608
  @click.option("-r", "--rootfs", required=False, help="Custom root fs folders (internal use only)")
522
- @click.option("-a", "--autoflash", is_flag=True, default=False, show_default=True, help="Net boot the DevKit and automatically flash the internal storage - TBD")
609
+ @click.option("-a", "--autoflash", is_flag=True, default=False, show_default=True, help="Network boot the selected DevKit, then automatically flash its internal storage once SSH is ready.")
523
610
  @click.pass_context
524
- def bootimg_cmd(ctx, version, boardtype, netboot, devkit_ip, autoflash, fwtype, rootfs):
611
+ def bootimg_cmd(ctx, version, boardtype, netboot, devkit_ip, autoflash, fwtype, rootfs, recovery=False, force=False):
525
612
  """
526
613
  Prepare a bootable image for the SiMa DevKit.
527
614
 
@@ -530,6 +617,10 @@ def bootimg_cmd(ctx, version, boardtype, netboot, devkit_ip, autoflash, fwtype,
530
617
  network boot environment. It supports both MLSoC- and Modalix-based
531
618
  DevKits, as well as Yocto and eLxr firmware types.
532
619
 
620
+ Matching internal builds appear in aligned Version and Build time (UTC)
621
+ columns, newest first. Build time uses the newest Artifactory archive
622
+ creation timestamp for each version. Unknown timestamps appear last.
623
+
533
624
  Operations Performed:
534
625
 
535
626
  • Download the correct firmware bundle for the selected version
@@ -555,27 +646,46 @@ def bootimg_cmd(ctx, version, boardtype, netboot, devkit_ip, autoflash, fwtype,
555
646
 
556
647
  # Write an SD card image for an MLSoC DevKit
557
648
 
558
- sima-cli bootimg -v 1.6.0 --boardtype mlsoc
649
+ sima-cli bootimg -v 1.6.0 --boardtype mlsoc --fwtype yocto
559
650
 
560
651
  # Set up netboot for a Modalix DevKit
561
652
 
562
- sima-cli bootimg -v 1.6.0 --boardtype modalix --netboot
653
+ sima-cli bootimg -v 3.0.0 --netboot
654
+
655
+ # Select a DevKit for confirmed remote U-Boot setup and reboot
656
+
657
+ sima-cli bootimg -v 3.0.0 --netboot --devkit-ip 192.168.1.20
563
658
 
564
- # Set up netboot and probe an existing DevKit first
659
+ # Allow daily mirror fallback for internal eLxr 3.0+ netboot
565
660
 
566
- sima-cli bootimg -v 2.1.0 --boardtype modalix --netboot --devkit-ip 192.168.1.20
661
+ sima-cli -i bootimg -v 1247 --netboot -f
567
662
 
568
663
  # Prepare an eLxr netboot image for Modalix
569
664
 
570
- sima-cli bootimg -v 2.0.0 --boardtype modalix --fwtype elxr --netboot
665
+ sima-cli bootimg -v 2.0.0 --netboot
666
+
667
+ # Prepare USB/SD recovery media that automatically recovers eMMC
668
+
669
+ sima-cli bootimg -v 3.0.0 --recovery
571
670
 
572
671
  """
573
672
 
673
+ if recovery:
674
+ if netboot or autoflash or rootfs or devkit_ip:
675
+ raise click.UsageError("--recovery cannot be combined with --netboot, --autoflash, --rootfs, or --devkit-ip.")
676
+ if ctx.get_parameter_source("boardtype") == click.core.ParameterSource.DEFAULT:
677
+ boardtype = "modalix"
678
+ if ctx.get_parameter_source("fwtype") == click.core.ParameterSource.DEFAULT:
679
+ fwtype = "elxr"
680
+ if boardtype != "modalix" or fwtype != "elxr":
681
+ raise click.UsageError("--recovery requires --boardtype modalix and --fwtype elxr.")
682
+
574
683
  from sima_cli.update.bootimg import write_image
575
684
  from sima_cli.update.netboot import setup_netboot
576
- from sima_cli.update.remote import get_remote_board_info
577
-
578
685
  internal = ctx.obj.get("internal", False)
686
+ if internal and ctx.meta.get('daily_netboot') and (boardtype != 'modalix' or fwtype != 'elxr'):
687
+ if not internal_resource_exists() or not ctx.obj.get('internal_reachable', True):
688
+ raise click.ClickException('Artifactory is unavailable. Daily netboot fallback requires Modalix eLxr.')
579
689
 
580
690
  click.echo(f"📦 Preparing boot image:")
581
691
  click.echo(f" 🔹 Version : {version}")
@@ -586,23 +696,13 @@ def bootimg_cmd(ctx, version, boardtype, netboot, devkit_ip, autoflash, fwtype,
586
696
  click.echo(f" 🔹 DevKit IP : {devkit_ip}")
587
697
 
588
698
  try:
589
- if devkit_ip and (netboot or autoflash):
590
- click.echo(f"🔎 Probing DevKit version at {devkit_ip} ...")
591
- _, remote_version, _, _, _ = get_remote_board_info(devkit_ip)
592
- if not remote_version:
593
- raise click.ClickException(
594
- f"Unable to retrieve remote version from DevKit at {devkit_ip}."
595
- )
596
- click.echo(f"✅ DevKit current version: {remote_version}")
597
-
598
699
  boardtype = boardtype if boardtype != 'mlsoc' else 'davinci'
599
700
  if netboot or autoflash:
600
- setup_netboot(version, boardtype, internal, autoflash, flavor='headless', rootfs=rootfs, swtype=fwtype)
701
+ setup_netboot(version, boardtype, internal, autoflash, flavor='headless', rootfs=rootfs, swtype=fwtype, allow_daily_fallback=force, devkit=devkit_ip)
601
702
  click.echo("✅ Netboot image prepared and TFTP server is running.")
602
703
  else:
603
- write_image(version, boardtype, fwtype, internal, flavor='headless')
704
+ write_image(version, boardtype, fwtype, internal, flavor='headless', recovery=recovery)
604
705
  click.echo("✅ Boot image successfully written.")
605
- click.echo("✅ Boot image successfully written.")
606
706
  except Exception as e:
607
707
  click.echo(f"❌ Failed to write boot image: {e}", err=True)
608
708
  ctx.exit(1)
@@ -616,6 +716,7 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
616
716
  NEAT_INSTALL_REPOSITORIES = {
617
717
  "apps",
618
718
  "core",
719
+ "edgematic-studio",
619
720
  "insight",
620
721
  "internals",
621
722
  "llima",
@@ -97,6 +97,8 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
97
97
  return download_github_asset(url, token=token)
98
98
  else:
99
99
  session = requests.Session()
100
+ if parsed_url.hostname == 'artifacts.neat.sima.ai':
101
+ session.trust_env = False
100
102
  request_fn = session.get
101
103
  head_fn = session.head
102
104
 
@@ -383,22 +383,35 @@ def _resolve_resource_url_candidates(base_url: str, resource: str) -> List[str]:
383
383
  return [primary_url, fallback_url]
384
384
 
385
385
  def _metadata_resource_path(dest_folder: str, resource: str, resource_url: str) -> Path:
386
+ install_root = Path(dest_folder).resolve()
387
+ if re.match(r"^[A-Za-z]:[\\/]", resource) or "\\" in resource:
388
+ raise click.ClickException(f"❌ Resource path must be a safe relative path: '{resource}'.")
389
+
386
390
  parsed_resource = urlparse(resource)
387
391
  if parsed_resource.scheme or parsed_resource.netloc:
388
392
  file_name = os.path.basename(urlparse(resource_url).path)
393
+ relative_parts = (file_name,)
389
394
  else:
390
- file_name = os.path.basename(resource)
395
+ relative_parts = tuple(resource.split("/"))
396
+ if resource.startswith("/") or any(part in {"", ".", ".."} for part in relative_parts):
397
+ raise click.ClickException(f"❌ Resource path must be a safe relative path: '{resource}'.")
398
+ file_name = relative_parts[-1]
391
399
 
392
400
  if not file_name:
393
401
  raise click.ClickException(f"❌ Cannot determine file name for resource '{resource}'.")
394
402
 
395
- return Path(dest_folder) / file_name
403
+ destination = Path(dest_folder).joinpath(*relative_parts)
404
+ resolved_destination = destination.resolve()
405
+ if resolved_destination != install_root and install_root not in resolved_destination.parents:
406
+ raise click.ClickException(f"❌ Resource path escapes the installation directory: '{resource}'.")
407
+ return destination
396
408
 
397
409
  def _normalize_downloaded_metadata_resource(local_path: str, expected_path: Path) -> str:
398
410
  downloaded_path = Path(local_path)
399
411
  if downloaded_path == expected_path:
400
412
  return local_path
401
413
 
414
+ expected_path.parent.mkdir(parents=True, exist_ok=True)
402
415
  if expected_path.exists():
403
416
  expected_path.unlink()
404
417
  downloaded_path.rename(expected_path)
@@ -416,7 +429,7 @@ def _download_metadata_file_resource(
416
429
  try:
417
430
  local_path = download_file_from_url(
418
431
  url=resource_url,
419
- dest_folder=dest_folder,
432
+ dest_folder=str(dest_path.parent),
420
433
  internal=internal
421
434
  )
422
435
  return _normalize_downloaded_metadata_resource(local_path, dest_path)
@@ -1045,7 +1058,11 @@ def _combine_multipart_files(folder: str, local_paths=None):
1045
1058
  folder_resolved = folder.resolve()
1046
1059
  for p in local_paths:
1047
1060
  path = Path(p).resolve()
1048
- if path.exists() and path.is_file() and path.parent == folder_resolved:
1061
+ if (
1062
+ path.exists()
1063
+ and path.is_file()
1064
+ and folder_resolved in path.parents
1065
+ ):
1049
1066
  candidate_files.append(path)
1050
1067
 
1051
1068
  for file in candidate_files:
@@ -1055,12 +1072,12 @@ def _combine_multipart_files(folder: str, local_paths=None):
1055
1072
  match = re.match(r"(.+)-split-([a-z]{2})$", file.name)
1056
1073
  if match:
1057
1074
  base, part = match.groups()
1058
- parts_by_base.setdefault(base, []).append((part, file))
1075
+ parts_by_base.setdefault((file.parent, base), []).append((part, file))
1059
1076
 
1060
1077
  # Step 2: Process each group
1061
- for base, parts in parts_by_base.items():
1078
+ for (parent, base), parts in parts_by_base.items():
1062
1079
  parts.sort(key=lambda x: x[0])
1063
- output_file = folder / f"{base}.tar"
1080
+ output_file = parent / f"{base}.tar"
1064
1081
  total_size = sum(part_file.stat().st_size for _, part_file in parts)
1065
1082
 
1066
1083
  print(f"\n🧩 Reassembling: {output_file.name} from {len(parts)} parts")
@@ -1089,7 +1106,7 @@ def _combine_multipart_files(folder: str, local_paths=None):
1089
1106
  print(f"✅ Created: {output_file.name} ({output_file.stat().st_size / 1e6:.2f} MB)")
1090
1107
 
1091
1108
  # Step 4: Auto-extract .tar
1092
- extract_dir = folder / base
1109
+ extract_dir = parent / base
1093
1110
  print(f"📦 Extracting {output_file.name} to {extract_dir}/")
1094
1111
  _extract_tar_streaming(output_file, extract_dir)
1095
1112
 
@@ -1106,18 +1123,18 @@ def _extract_archives_in_folder(folder: str, local_paths):
1106
1123
  file = Path(local_path).resolve()
1107
1124
  if not file.exists() or not file.is_file():
1108
1125
  continue
1109
- if file.parent != folder:
1126
+ if folder not in file.parents:
1110
1127
  continue
1111
1128
 
1112
1129
  # TAR, GZ, TAR.GZ → all handled by _extract_tar_streaming
1113
1130
  if file.suffix in [".tar", ".gz"] or file.name.endswith(".tar.gz"):
1114
- extract_dir = folder / file.stem.replace(".tar", "")
1131
+ extract_dir = file.parent / file.stem.replace(".tar", "")
1115
1132
  print(f"📦 Extracting TAR/GZ: {file.name} to {extract_dir}/")
1116
1133
  _extract_tar_streaming(file, extract_dir)
1117
1134
 
1118
1135
  # ZIP
1119
1136
  elif file.suffix == ".zip":
1120
- extract_dir = folder / file.stem
1137
+ extract_dir = file.parent / file.stem
1121
1138
  print(f"📦 Extracting ZIP: {file.name} to {extract_dir}/")
1122
1139
  _extract_zip_streaming(file, extract_dir)
1123
1140
 
@@ -0,0 +1,3 @@
1
+ from .commands import register_models_commands
2
+
3
+ __all__ = ["register_models_commands"]