sima-cli 2.1.6__tar.gz → 2.1.8__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 (140) hide show
  1. {sima_cli-2.1.6/sima_cli.egg-info → sima_cli-2.1.8}/PKG-INFO +18 -5
  2. {sima_cli-2.1.6 → sima_cli-2.1.8}/README.md +17 -4
  3. {sima_cli-2.1.6 → sima_cli-2.1.8}/pyproject.toml +1 -1
  4. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/__version__.py +1 -1
  5. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/cli.py +81 -5
  6. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/metadata_installer.py +202 -8
  7. sima_cli-2.1.8/sima_cli/install/package_builder.py +308 -0
  8. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/registry.py +66 -0
  9. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/commands.py +1 -1
  10. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/install.py +218 -0
  11. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/neat.py +37 -6
  12. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/preinstall.py +10 -3
  13. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/utils.py +4 -1
  14. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/netboot.py +126 -24
  15. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/updater.py +11 -16
  16. sima_cli-2.1.8/sima_cli/upgrade/selfupdate.py +408 -0
  17. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/vulcan/artifacts.py +99 -6
  18. sima_cli-2.1.8/sima_cli/vulcan/commands.py +304 -0
  19. {sima_cli-2.1.6 → sima_cli-2.1.8/sima_cli.egg-info}/PKG-INFO +18 -5
  20. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli.egg-info/SOURCES.txt +4 -0
  21. sima_cli-2.1.8/tests/unit/test_firmware.py +60 -0
  22. sima_cli-2.1.8/tests/unit/test_metadata_installer.py +207 -0
  23. sima_cli-2.1.8/tests/unit/test_netboot.py +98 -0
  24. sima_cli-2.1.8/tests/unit/test_package_builder.py +359 -0
  25. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_sdk_image_detection.py +281 -0
  26. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_sdk_preinstall.py +14 -0
  27. sima_cli-2.1.8/tests/unit/test_selfupdate.py +181 -0
  28. sima_cli-2.1.8/tests/unit/test_vulcan.py +524 -0
  29. sima_cli-2.1.6/sima_cli/upgrade/selfupdate.py +0 -236
  30. sima_cli-2.1.6/sima_cli/vulcan/commands.py +0 -115
  31. sima_cli-2.1.6/tests/unit/test_firmware.py +0 -0
  32. sima_cli-2.1.6/tests/unit/test_selfupdate.py +0 -68
  33. sima_cli-2.1.6/tests/unit/test_vulcan.py +0 -180
  34. {sima_cli-2.1.6 → sima_cli-2.1.8}/LICENSE +0 -0
  35. {sima_cli-2.1.6 → sima_cli-2.1.8}/MANIFEST.in +0 -0
  36. {sima_cli-2.1.6 → sima_cli-2.1.8}/requirements.txt +0 -0
  37. {sima_cli-2.1.6 → sima_cli-2.1.8}/setup.cfg +0 -0
  38. {sima_cli-2.1.6 → sima_cli-2.1.8}/setup.py +0 -0
  39. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/__init__.py +0 -0
  40. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/__main__.py +0 -0
  41. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/app_zoo/__init__.py +0 -0
  42. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/app_zoo/app.py +0 -0
  43. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/app_zoo/commands.py +0 -0
  44. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/auth/__init__.py +0 -0
  45. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/auth/auth0.py +0 -0
  46. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/auth/devportal.py +0 -0
  47. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/auth/login.py +0 -0
  48. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/auth/oauth.py +0 -0
  49. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/data/__init__.py +0 -0
  50. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/data/resources_internal.yaml +0 -0
  51. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/data/resources_public.yaml +0 -0
  52. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/deploy_only/__init__.py +0 -0
  53. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/deploy_only/device/__init__.py +0 -0
  54. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/deploy_only/device/commands.py +0 -0
  55. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  56. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/deploy_only/mpk/commands.py +0 -0
  57. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/discover/__init__.py +0 -0
  58. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/discover/discover.py +0 -0
  59. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/discover/linuxll.py +0 -0
  60. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/download/__init__.py +0 -0
  61. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/download/downloader.py +0 -0
  62. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/__init__.py +0 -0
  63. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/github_assets.py +0 -0
  64. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/hostdriver.py +0 -0
  65. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/metadata_info.py +0 -0
  66. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/metadata_validator.py +0 -0
  67. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/optiview.py +0 -0
  68. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/install/palette.py +0 -0
  69. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/mla/__init__.py +0 -0
  70. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/mla/meminfo.py +0 -0
  71. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/model_zoo/__init__.py +0 -0
  72. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/model_zoo/model.py +0 -0
  73. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/network/__init__.py +0 -0
  74. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/network/network.py +0 -0
  75. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/playbooks/__init__.py +0 -0
  76. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/playbooks/commands.py +0 -0
  77. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/playbooks/manager.py +0 -0
  78. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/__init__.py +0 -0
  79. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/cmdexec.py +0 -0
  80. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/config.py +0 -0
  81. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/linux_shared_network.py +0 -0
  82. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/requirements.json +0 -0
  83. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/script.py +0 -0
  84. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/stop.py +0 -0
  85. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/sdk/uninstall.py +0 -0
  86. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/serial/__init__.py +0 -0
  87. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/serial/serial.py +0 -0
  88. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/storage/__init__.py +0 -0
  89. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/storage/nvme.py +0 -0
  90. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/storage/sdcard.py +0 -0
  91. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/__init__.py +0 -0
  92. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/bmaptool.py +0 -0
  93. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/bootimg.py +0 -0
  94. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/cleanlog.py +0 -0
  95. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/elxr.py +0 -0
  96. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/local.py +0 -0
  97. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/query.py +0 -0
  98. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/update/remote.py +0 -0
  99. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/upgrade/__init__.py +0 -0
  100. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/__init__.py +0 -0
  101. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/api_common.py +0 -0
  102. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/artifactory.py +0 -0
  103. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/common.py +0 -0
  104. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/config.py +0 -0
  105. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/config_loader.py +0 -0
  106. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/container_registries.py +0 -0
  107. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/device_api.py +0 -0
  108. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/disk.py +0 -0
  109. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/docker.py +0 -0
  110. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/env.py +0 -0
  111. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/errors.py +0 -0
  112. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/mpk_api.py +0 -0
  113. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/net.py +0 -0
  114. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/network.py +0 -0
  115. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/pcie.py +0 -0
  116. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/pkg_update_check.py +0 -0
  117. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/serializers.py +0 -0
  118. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/services.py +0 -0
  119. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/utils/tag.py +0 -0
  120. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli/vulcan/__init__.py +0 -0
  121. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli.egg-info/dependency_links.txt +0 -0
  122. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli.egg-info/entry_points.txt +0 -0
  123. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli.egg-info/requires.txt +0 -0
  124. {sima_cli-2.1.6 → sima_cli-2.1.8}/sima_cli.egg-info/top_level.txt +0 -0
  125. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/__init__.py +0 -0
  126. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/e2e/__init__.py +0 -0
  127. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/__init__.py +0 -0
  128. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_app_zoo.py +0 -0
  129. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_auth.py +0 -0
  130. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_cli.py +0 -0
  131. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_cli_stdio.py +0 -0
  132. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_download.py +0 -0
  133. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_elxr_update.py +0 -0
  134. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_install_stub.py +0 -0
  135. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_model_zoo.py +0 -0
  136. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_pkg_update_check.py +0 -0
  137. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_sdk_uninstall.py +0 -0
  138. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_skills_commands.py +0 -0
  139. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_skills_manager.py +0 -0
  140. {sima_cli-2.1.6 → sima_cli-2.1.8}/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.6
3
+ Version: 2.1.8
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://developer.sima.ai/
6
6
  Author: SiMa.ai
@@ -163,10 +163,11 @@ sima-cli vulcan download --env production core main
163
163
  - `dev`: `https://artifacts.neat.paconsultings.com`
164
164
  - `staging`: `https://artifacts.stg.neat.sima.ai`
165
165
  - `production`: `https://artifacts.neat.sima.ai`
166
- - `staging` and `production` are not yet available for Vulcan downloads; use `--env dev` for now.
166
+ - `dev` and `staging` are available for Vulcan downloads. `production` is not yet available.
167
167
  - Usage:
168
- - `sima-cli vulcan --env {dev|staging|production} download [REPO] [BRANCH_OR_TAG]`
169
- - `sima-cli vulcan download --env {dev|staging|production} [REPO] [BRANCH_OR_TAG]`
168
+ - `sima-cli vulcan --env {dev|stg|staging|prd|prod|production} download [REPO] [BRANCH_OR_TAG]`
169
+ - `sima-cli vulcan download --env {dev|stg|staging|prd|prod|production} [REPO] [BRANCH_OR_TAG]`
170
+ - Shortcut flags are also available: `--dev`, `--stg`/`--staging`, and `--prd`/`--prod`.
170
171
  - If `REPO` is omitted, the CLI prompts for a repository.
171
172
  - If `BRANCH_OR_TAG` is omitted, the CLI downloads `branches.json` and prompts for a branch.
172
173
  - For automation, pass both values and add `--json` for structured output.
@@ -587,7 +588,19 @@ sima-cli selfupdate
587
588
  sima-cli selfupdate --dev
588
589
  ```
589
590
 
590
- - Update from the latest tested artifact installer. On Windows, this prints the PowerShell commands to run in a new shell.
591
+ - Update from Vulcan dev artifacts. If `--branch` is omitted, sima-cli loads `branches.json` and prompts for a branch.
592
+
593
+ ```bash
594
+ sima-cli selfupdate --stg --branch main
595
+ ```
596
+
597
+ - Update from Vulcan staging artifacts.
598
+
599
+ ```bash
600
+ sima-cli selfupdate --neat --branch main
601
+ ```
602
+
603
+ - Update from Vulcan production artifacts. Production aliases are `--prd`, `--prod`, `--neat`, and `--vulcan`.
591
604
 
592
605
  ```bash
593
606
  sima-cli selfupdate -v 0.0.46
@@ -129,10 +129,11 @@ sima-cli vulcan download --env production core main
129
129
  - `dev`: `https://artifacts.neat.paconsultings.com`
130
130
  - `staging`: `https://artifacts.stg.neat.sima.ai`
131
131
  - `production`: `https://artifacts.neat.sima.ai`
132
- - `staging` and `production` are not yet available for Vulcan downloads; use `--env dev` for now.
132
+ - `dev` and `staging` are available for Vulcan downloads. `production` is not yet available.
133
133
  - Usage:
134
- - `sima-cli vulcan --env {dev|staging|production} download [REPO] [BRANCH_OR_TAG]`
135
- - `sima-cli vulcan download --env {dev|staging|production} [REPO] [BRANCH_OR_TAG]`
134
+ - `sima-cli vulcan --env {dev|stg|staging|prd|prod|production} download [REPO] [BRANCH_OR_TAG]`
135
+ - `sima-cli vulcan download --env {dev|stg|staging|prd|prod|production} [REPO] [BRANCH_OR_TAG]`
136
+ - Shortcut flags are also available: `--dev`, `--stg`/`--staging`, and `--prd`/`--prod`.
136
137
  - If `REPO` is omitted, the CLI prompts for a repository.
137
138
  - If `BRANCH_OR_TAG` is omitted, the CLI downloads `branches.json` and prompts for a branch.
138
139
  - For automation, pass both values and add `--json` for structured output.
@@ -553,7 +554,19 @@ sima-cli selfupdate
553
554
  sima-cli selfupdate --dev
554
555
  ```
555
556
 
556
- - Update from the latest tested artifact installer. On Windows, this prints the PowerShell commands to run in a new shell.
557
+ - Update from Vulcan dev artifacts. If `--branch` is omitted, sima-cli loads `branches.json` and prompts for a branch.
558
+
559
+ ```bash
560
+ sima-cli selfupdate --stg --branch main
561
+ ```
562
+
563
+ - Update from Vulcan staging artifacts.
564
+
565
+ ```bash
566
+ sima-cli selfupdate --neat --branch main
567
+ ```
568
+
569
+ - Update from Vulcan production artifacts. Production aliases are `--prd`, `--prod`, `--neat`, and `--vulcan`.
557
570
 
558
571
  ```bash
559
572
  sima-cli selfupdate -v 0.0.46
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.6"
7
+ version = "2.1.8"
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"
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.6"
2
+ __version__ = "2.1.8"
@@ -30,6 +30,13 @@ from sima_cli.install.registry import register_packages_commands
30
30
  from sima_cli.upgrade.selfupdate import register_selfupdate_command
31
31
  from sima_cli.playbooks import register_playbook_commands
32
32
  from sima_cli.vulcan import register_vulcan_commands
33
+ from sima_cli.vulcan.commands import (
34
+ ENV_METAVAR,
35
+ _environment_shortcut_options,
36
+ _normalize_environment_option,
37
+ _resolve_environment,
38
+ install_vulcan_package,
39
+ )
33
40
 
34
41
  def _configure_stdio_errors() -> None:
35
42
  for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
@@ -504,7 +511,39 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
504
511
  @click.argument("component", required=False)
505
512
  @click.option("-v", "--version", help="SDK version (required for SDK-dependent components unless --metadata is provided)")
506
513
  @click.option("-m", "--mirror", help="URL to a metadata.json file for generic installation")
507
- @click.option("-t", "--tag", help="Tag of the package (optional)")
514
+ @click.option("-t", "--tag", help="Tag of the package. With --neat, metadata variant type such as minimum.")
515
+ @click.option("--neat", "use_neat", is_flag=True, help="Install from Neat artifacts using the Neat package resolver.")
516
+ @click.option(
517
+ "--vulcan",
518
+ "use_vulcan",
519
+ is_flag=True,
520
+ help="Install from Neat/Vulcan artifacts. Compatibility alias for --neat.",
521
+ )
522
+ @_environment_shortcut_options
523
+ @click.option(
524
+ "--env",
525
+ "vulcan_environment",
526
+ metavar=ENV_METAVAR,
527
+ callback=_normalize_environment_option,
528
+ default=None,
529
+ help="Neat artifact environment. Used with --neat or --vulcan. Defaults to production.",
530
+ )
531
+ @click.option(
532
+ "--base-url",
533
+ "vulcan_base_url",
534
+ default=None,
535
+ envvar="SIMA_NEAT_BASE_URL",
536
+ help="Override the Neat artifact base URL. Used with --neat or --vulcan.",
537
+ )
538
+ @click.option(
539
+ "-d",
540
+ "--install-dir",
541
+ default=".",
542
+ show_default=True,
543
+ type=click.Path(file_okay=False, dir_okay=True, path_type=str),
544
+ help="Directory where Neat package resources are downloaded and installed. Used with --neat or --vulcan.",
545
+ )
546
+ @click.option("--json", "json_output", is_flag=True, help="With --neat or --vulcan, print resolved metadata URL and exit.")
508
547
  @click.option(
509
548
  "-f",
510
549
  "--force",
@@ -513,7 +552,21 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
513
552
  help="Force installation even if compatibility checks fail.",
514
553
  )
515
554
  @click.pass_context
516
- def install_cmd(ctx, component, version, mirror, tag, force):
555
+ def install_cmd(
556
+ ctx,
557
+ component,
558
+ version,
559
+ mirror,
560
+ tag,
561
+ use_neat,
562
+ use_vulcan,
563
+ vulcan_environment,
564
+ environment_flag,
565
+ vulcan_base_url,
566
+ install_dir,
567
+ json_output,
568
+ force,
569
+ ):
517
570
  """
518
571
  Install SiMa packages.
519
572
 
@@ -548,12 +601,32 @@ def install_cmd(ctx, component, version, mirror, tag, force):
548
601
  """
549
602
  internal = ctx.obj.get("internal", False)
550
603
 
604
+ use_neat_resolver = use_neat or use_vulcan
605
+ if use_neat_resolver:
606
+ if use_neat and use_vulcan:
607
+ raise click.ClickException("Use only one of --neat or --vulcan.")
608
+ if mirror:
609
+ raise click.ClickException("--mirror cannot be used with --neat.")
610
+ if not component:
611
+ raise click.ClickException("You must specify a Neat target when using --neat.")
612
+ install_vulcan_package(
613
+ target=component,
614
+ environment=_resolve_environment(vulcan_environment, environment_flag),
615
+ base_url=vulcan_base_url or os.getenv("SIMA_VULCAN_BASE_URL"),
616
+ package_type=tag,
617
+ install_dir=install_dir,
618
+ force=force,
619
+ json_output=json_output,
620
+ )
621
+ return None
622
+
551
623
  # Metadata-based installation path
552
624
  if mirror:
553
625
  if component:
554
626
  click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
555
627
  click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
556
- return install_from_metadata(metadata_url=mirror, internal=internal, force=force)
628
+ install_from_metadata(metadata_url=mirror, internal=internal, force=force)
629
+ return None
557
630
 
558
631
  # No component and no metadata: error
559
632
  if not component:
@@ -562,11 +635,13 @@ def install_cmd(ctx, component, version, mirror, tag, force):
562
635
 
563
636
  # if user specified gh: as component, treat it the same as -m
564
637
  if component.startswith("gh:"):
565
- return install_from_metadata(metadata_url=component, internal=False, force=force)
638
+ install_from_metadata(metadata_url=component, internal=False, force=force)
639
+ return None
566
640
 
567
641
  # if the user specified cr: or ghcr: as component, install from container registry
568
642
  if component.startswith("cr:") or component.startswith("ghcr:"):
569
- return install_from_cr(resource_spec=component, internal=internal)
643
+ install_from_cr(resource_spec=component, internal=internal)
644
+ return None
570
645
 
571
646
  version = resolve_version(version)
572
647
 
@@ -601,6 +676,7 @@ def install_cmd(ctx, component, version, mirror, tag, force):
601
676
  ctx.exit(1)
602
677
 
603
678
  click.echo("✅ Installation complete.")
679
+ return None
604
680
 
605
681
 
606
682
  # ----------------------
@@ -11,7 +11,7 @@ import stat
11
11
  import shlex
12
12
  import platform
13
13
  import hashlib
14
- from urllib.parse import urlparse, quote, urljoin
14
+ from urllib.parse import urlparse, quote, urljoin, unquote
15
15
  from typing import Dict
16
16
  from tqdm import tqdm
17
17
  from pathlib import Path
@@ -280,6 +280,169 @@ def _compute_sha256(file_path: Path, chunk_size: int = 1024 * 1024) -> str:
280
280
  hasher.update(chunk)
281
281
  return hasher.hexdigest()
282
282
 
283
+ def _resolve_resource_url(base_url: str, resource: str) -> str:
284
+ """
285
+ Resolve a metadata resource to a downloadable URL.
286
+
287
+ Metadata resources are file names or relative paths. Encode each path segment
288
+ so URL-reserved characters that are valid in artifact names, such as '+', do
289
+ not get interpreted by object storage/CDNs as a different key.
290
+ """
291
+ parsed_resource = urlparse(resource)
292
+ if parsed_resource.scheme or parsed_resource.netloc:
293
+ return resource
294
+
295
+ encoded_resource = "/".join(
296
+ quote(segment, safe="")
297
+ for segment in resource.split("/")
298
+ )
299
+ return urljoin(base_url, encoded_resource)
300
+
301
+ def _resolve_resource_url_candidates(base_url: str, resource: str) -> list[str]:
302
+ primary_url = _resolve_resource_url(base_url, resource)
303
+
304
+ parsed_resource = urlparse(resource)
305
+ if parsed_resource.scheme or parsed_resource.netloc or "%" not in resource:
306
+ return [primary_url]
307
+
308
+ percent_preserving_resource = "/".join(
309
+ quote(segment, safe="%")
310
+ for segment in resource.split("/")
311
+ )
312
+ fallback_url = urljoin(base_url, percent_preserving_resource)
313
+ if fallback_url == primary_url:
314
+ return [primary_url]
315
+ return [primary_url, fallback_url]
316
+
317
+ def _metadata_resource_path(dest_folder: str, resource: str, resource_url: str) -> Path:
318
+ parsed_resource = urlparse(resource)
319
+ if parsed_resource.scheme or parsed_resource.netloc:
320
+ file_name = os.path.basename(urlparse(resource_url).path)
321
+ else:
322
+ file_name = os.path.basename(resource)
323
+
324
+ if not file_name:
325
+ raise click.ClickException(f"❌ Cannot determine file name for resource '{resource}'.")
326
+
327
+ return Path(dest_folder) / file_name
328
+
329
+ def _normalize_downloaded_metadata_resource(local_path: str, expected_path: Path) -> str:
330
+ downloaded_path = Path(local_path)
331
+ if downloaded_path == expected_path:
332
+ return local_path
333
+
334
+ if expected_path.exists():
335
+ expected_path.unlink()
336
+ downloaded_path.rename(expected_path)
337
+ return str(expected_path)
338
+
339
+ def _download_metadata_file_resource(
340
+ resource: str,
341
+ resource_urls: list[str],
342
+ dest_folder: str,
343
+ dest_path: Path,
344
+ internal: bool,
345
+ ) -> str:
346
+ errors: list[str] = []
347
+ for index, resource_url in enumerate(resource_urls):
348
+ try:
349
+ local_path = download_file_from_url(
350
+ url=resource_url,
351
+ dest_folder=dest_folder,
352
+ internal=internal
353
+ )
354
+ return _normalize_downloaded_metadata_resource(local_path, dest_path)
355
+ except Exception as e:
356
+ errors.append(f"{resource_url}: {e}")
357
+ if index < len(resource_urls) - 1:
358
+ click.echo(
359
+ f"⚠️ Download failed for encoded resource URL; retrying percent-preserving URL for '{resource}'."
360
+ )
361
+
362
+ raise click.ClickException("; ".join(errors))
363
+
364
+
365
+ def _resource_basename(resource: str) -> str:
366
+ parsed = urlparse(resource)
367
+ path = parsed.path if parsed.scheme or parsed.netloc else resource
368
+ return unquote(os.path.basename(path)).lower()
369
+
370
+
371
+ def _is_wheel_resource(resource: str) -> bool:
372
+ return _resource_basename(resource).endswith(".whl")
373
+
374
+
375
+ def _wheel_platform_tag(resource: str) -> str:
376
+ name = _resource_basename(resource)
377
+ if not name.endswith(".whl"):
378
+ return ""
379
+ parts = name[:-4].split("-")
380
+ if len(parts) < 5:
381
+ return ""
382
+ return parts[-1].lower()
383
+
384
+
385
+ def _current_wheel_platform() -> tuple:
386
+ system = platform.system().lower()
387
+ machine = platform.machine().lower()
388
+
389
+ if system == "darwin":
390
+ os_family = "mac"
391
+ elif system == "windows":
392
+ os_family = "windows"
393
+ elif system == "linux":
394
+ os_family = "linux"
395
+ else:
396
+ os_family = system
397
+
398
+ if machine in {"x86_64", "amd64"}:
399
+ arch_tokens = {"x86_64", "amd64"}
400
+ elif machine in {"aarch64", "arm64"}:
401
+ arch_tokens = {"aarch64", "arm64"}
402
+ elif machine in {"i386", "i686", "x86"}:
403
+ arch_tokens = {"i386", "i686", "x86", "win32"}
404
+ else:
405
+ arch_tokens = {machine} if machine else set()
406
+
407
+ return os_family, arch_tokens
408
+
409
+
410
+ def _wheel_arch_matches(platform_tag: str, arch_tokens: set) -> bool:
411
+ known_arch_tokens = {"x86_64", "amd64", "aarch64", "arm64", "i386", "i686", "x86", "win32"}
412
+ present_tokens = {token for token in known_arch_tokens if token in platform_tag}
413
+ return not present_tokens or bool(present_tokens & arch_tokens)
414
+
415
+
416
+ def _is_compatible_wheel_resource(resource: str) -> bool:
417
+ platform_tag = _wheel_platform_tag(resource)
418
+ if not platform_tag:
419
+ return True
420
+ if platform_tag in {"any", "none"}:
421
+ return True
422
+
423
+ os_family, arch_tokens = _current_wheel_platform()
424
+ if os_family == "mac":
425
+ os_matches = platform_tag.startswith("macosx") or "macos" in platform_tag
426
+ elif os_family == "windows":
427
+ os_matches = platform_tag.startswith("win") or platform_tag in {"win32", "win_amd64", "win_arm64"}
428
+ elif os_family == "linux":
429
+ os_matches = "linux" in platform_tag
430
+ else:
431
+ os_matches = False
432
+
433
+ return os_matches and _wheel_arch_matches(platform_tag, arch_tokens)
434
+
435
+
436
+ def _filter_download_compatible_resources(resources: list) -> list:
437
+ filtered = []
438
+ for resource in resources:
439
+ if _is_wheel_resource(resource) and not _is_compatible_wheel_resource(resource):
440
+ click.echo(f"⏭️ Skipping incompatible wheel for this platform: {resource}")
441
+ continue
442
+ filtered.append(resource)
443
+ return filtered
444
+
445
+
283
446
  def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal: bool = False, skip_models: bool = False, tag: str = None) -> list:
284
447
  """
285
448
  Downloads resources defined in metadata to a local destination folder.
@@ -321,8 +484,11 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
321
484
  continue
322
485
  filtered_resources.append(r)
323
486
 
487
+ if metadata.get("download-compatible-files-only"):
488
+ filtered_resources = _filter_download_compatible_resources(filtered_resources)
489
+
324
490
  if not filtered_resources:
325
- click.echo("ℹ️ No non-model resources to download.")
491
+ click.echo("ℹ️ No compatible resources to download.")
326
492
  return []
327
493
 
328
494
  click.echo(f"📥 Downloading {len(filtered_resources)} resource(s) to: {dest_folder}\n")
@@ -387,10 +553,8 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
387
553
  continue
388
554
 
389
555
  # 🌐 Standard file or URL
390
- resource_url = urljoin(base_url, resource)
391
- parsed = urlparse(resource_url)
392
- file_name = os.path.basename(parsed.path)
393
- dest_path = Path(dest_folder) / file_name
556
+ resource_urls = _resolve_resource_url_candidates(base_url, resource)
557
+ dest_path = _metadata_resource_path(dest_folder, resource, resource_urls[0])
394
558
 
395
559
  if expected_sha256 and dest_path.exists() and dest_path.is_file():
396
560
  existing_sha = _compute_sha256(dest_path)
@@ -398,9 +562,11 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
398
562
  click.echo(f"♻️ Checksum mismatch for existing file '{dest_path.name}', re-downloading.")
399
563
  dest_path.unlink()
400
564
 
401
- local_path = download_file_from_url(
402
- url=resource_url,
565
+ local_path = _download_metadata_file_resource(
566
+ resource=resource,
567
+ resource_urls=resource_urls,
403
568
  dest_folder=dest_folder,
569
+ dest_path=dest_path,
404
570
  internal=internal
405
571
  )
406
572
  if expected_sha256:
@@ -417,6 +583,28 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
417
583
 
418
584
  return local_paths
419
585
 
586
+ def _mark_install_script_executable(metadata: Dict, install_dir: str) -> None:
587
+ script = metadata.get("installation", {}).get("script", "")
588
+ if not isinstance(script, str):
589
+ return
590
+
591
+ script = script.strip()
592
+ if not script or any(char in script for char in "\n\r;&|`$<>"):
593
+ return
594
+
595
+ script_path = Path(script)
596
+ if not script_path.is_absolute():
597
+ script_path = Path(install_dir) / script_path
598
+ try:
599
+ resolved = script_path.resolve()
600
+ install_root = Path(install_dir).resolve()
601
+ if resolved != install_root and install_root not in resolved.parents:
602
+ return
603
+ if resolved.is_file():
604
+ resolved.chmod(resolved.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
605
+ except OSError:
606
+ return
607
+
420
608
  def selectable_resource_handler(metadata):
421
609
  """
422
610
  Allow user to select one or more opt-in resources to download.
@@ -936,6 +1124,10 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
936
1124
  platforms = metadata.get("platforms", [])
937
1125
  board_ver, _ = get_sima_build_version()
938
1126
 
1127
+ if not platforms:
1128
+ click.echo("ℹ️ No platform restrictions specified; treating package as compatible with all platforms.")
1129
+ return True
1130
+
939
1131
  # Detect current OS and version
940
1132
  os_name = platform.system().lower() # 'darwin', 'windows', 'linux'
941
1133
  os_version = "Unknown"
@@ -1218,8 +1410,10 @@ def install_from_metadata(metadata_url: str, internal: bool, install_dir: str =
1218
1410
  if _is_platform_compatible(metadata, force) or force:
1219
1411
  local_paths = _download_assets(metadata, metadata_url, install_dir, internal, tag=tag)
1220
1412
  if len(local_paths) > 0:
1413
+ _mark_install_script_executable(metadata, install_dir)
1221
1414
  _combine_multipart_files(install_dir, local_paths=local_paths)
1222
1415
  _extract_archives_in_folder(install_dir, local_paths)
1416
+ _mark_install_script_executable(metadata, install_dir)
1223
1417
  _run_installation_script(metadata=metadata, extract_path=install_dir)
1224
1418
 
1225
1419
  except Exception as e: