sima-cli 2.1.7__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 (138) hide show
  1. {sima_cli-2.1.7/sima_cli.egg-info → sima_cli-2.1.8}/PKG-INFO +18 -5
  2. {sima_cli-2.1.7 → sima_cli-2.1.8}/README.md +17 -4
  3. {sima_cli-2.1.7 → sima_cli-2.1.8}/pyproject.toml +1 -1
  4. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/__version__.py +1 -1
  5. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/cli.py +46 -15
  6. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/metadata_installer.py +198 -8
  7. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/package_builder.py +5 -1
  8. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/registry.py +18 -1
  9. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/install.py +42 -5
  10. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/utils.py +2 -0
  11. sima_cli-2.1.8/sima_cli/upgrade/selfupdate.py +408 -0
  12. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/vulcan/artifacts.py +1 -28
  13. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/vulcan/commands.py +114 -36
  14. {sima_cli-2.1.7 → sima_cli-2.1.8/sima_cli.egg-info}/PKG-INFO +18 -5
  15. sima_cli-2.1.8/tests/unit/test_metadata_installer.py +207 -0
  16. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_package_builder.py +45 -0
  17. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_sdk_image_detection.py +70 -0
  18. sima_cli-2.1.8/tests/unit/test_selfupdate.py +181 -0
  19. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_vulcan.py +108 -29
  20. sima_cli-2.1.7/sima_cli/upgrade/selfupdate.py +0 -236
  21. sima_cli-2.1.7/tests/unit/test_metadata_installer.py +0 -21
  22. sima_cli-2.1.7/tests/unit/test_selfupdate.py +0 -68
  23. {sima_cli-2.1.7 → sima_cli-2.1.8}/LICENSE +0 -0
  24. {sima_cli-2.1.7 → sima_cli-2.1.8}/MANIFEST.in +0 -0
  25. {sima_cli-2.1.7 → sima_cli-2.1.8}/requirements.txt +0 -0
  26. {sima_cli-2.1.7 → sima_cli-2.1.8}/setup.cfg +0 -0
  27. {sima_cli-2.1.7 → sima_cli-2.1.8}/setup.py +0 -0
  28. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/__init__.py +0 -0
  29. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/__main__.py +0 -0
  30. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/app_zoo/__init__.py +0 -0
  31. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/app_zoo/app.py +0 -0
  32. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/app_zoo/commands.py +0 -0
  33. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/auth/__init__.py +0 -0
  34. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/auth/auth0.py +0 -0
  35. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/auth/devportal.py +0 -0
  36. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/auth/login.py +0 -0
  37. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/auth/oauth.py +0 -0
  38. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/data/__init__.py +0 -0
  39. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/data/resources_internal.yaml +0 -0
  40. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/data/resources_public.yaml +0 -0
  41. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/deploy_only/__init__.py +0 -0
  42. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/deploy_only/device/__init__.py +0 -0
  43. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/deploy_only/device/commands.py +0 -0
  44. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  45. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/deploy_only/mpk/commands.py +0 -0
  46. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/discover/__init__.py +0 -0
  47. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/discover/discover.py +0 -0
  48. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/discover/linuxll.py +0 -0
  49. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/download/__init__.py +0 -0
  50. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/download/downloader.py +0 -0
  51. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/__init__.py +0 -0
  52. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/github_assets.py +0 -0
  53. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/hostdriver.py +0 -0
  54. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/metadata_info.py +0 -0
  55. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/metadata_validator.py +0 -0
  56. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/optiview.py +0 -0
  57. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/install/palette.py +0 -0
  58. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/mla/__init__.py +0 -0
  59. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/mla/meminfo.py +0 -0
  60. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/model_zoo/__init__.py +0 -0
  61. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/model_zoo/model.py +0 -0
  62. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/network/__init__.py +0 -0
  63. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/network/network.py +0 -0
  64. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/playbooks/__init__.py +0 -0
  65. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/playbooks/commands.py +0 -0
  66. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/playbooks/manager.py +0 -0
  67. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/__init__.py +0 -0
  68. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/cmdexec.py +0 -0
  69. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/commands.py +0 -0
  70. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/config.py +0 -0
  71. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/linux_shared_network.py +0 -0
  72. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/neat.py +0 -0
  73. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/preinstall.py +0 -0
  74. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/requirements.json +0 -0
  75. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/script.py +0 -0
  76. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/stop.py +0 -0
  77. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/sdk/uninstall.py +0 -0
  78. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/serial/__init__.py +0 -0
  79. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/serial/serial.py +0 -0
  80. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/storage/__init__.py +0 -0
  81. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/storage/nvme.py +0 -0
  82. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/storage/sdcard.py +0 -0
  83. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/__init__.py +0 -0
  84. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/bmaptool.py +0 -0
  85. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/bootimg.py +0 -0
  86. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/cleanlog.py +0 -0
  87. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/elxr.py +0 -0
  88. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/local.py +0 -0
  89. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/netboot.py +0 -0
  90. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/query.py +0 -0
  91. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/remote.py +0 -0
  92. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/update/updater.py +0 -0
  93. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/upgrade/__init__.py +0 -0
  94. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/__init__.py +0 -0
  95. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/api_common.py +0 -0
  96. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/artifactory.py +0 -0
  97. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/common.py +0 -0
  98. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/config.py +0 -0
  99. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/config_loader.py +0 -0
  100. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/container_registries.py +0 -0
  101. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/device_api.py +0 -0
  102. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/disk.py +0 -0
  103. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/docker.py +0 -0
  104. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/env.py +0 -0
  105. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/errors.py +0 -0
  106. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/mpk_api.py +0 -0
  107. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/net.py +0 -0
  108. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/network.py +0 -0
  109. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/pcie.py +0 -0
  110. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/pkg_update_check.py +0 -0
  111. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/serializers.py +0 -0
  112. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/services.py +0 -0
  113. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/utils/tag.py +0 -0
  114. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli/vulcan/__init__.py +0 -0
  115. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli.egg-info/SOURCES.txt +0 -0
  116. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli.egg-info/dependency_links.txt +0 -0
  117. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli.egg-info/entry_points.txt +0 -0
  118. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli.egg-info/requires.txt +0 -0
  119. {sima_cli-2.1.7 → sima_cli-2.1.8}/sima_cli.egg-info/top_level.txt +0 -0
  120. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/__init__.py +0 -0
  121. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/e2e/__init__.py +0 -0
  122. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/__init__.py +0 -0
  123. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_app_zoo.py +0 -0
  124. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_auth.py +0 -0
  125. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_cli.py +0 -0
  126. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_cli_stdio.py +0 -0
  127. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_download.py +0 -0
  128. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_elxr_update.py +0 -0
  129. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_firmware.py +0 -0
  130. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_install_stub.py +0 -0
  131. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_model_zoo.py +0 -0
  132. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_netboot.py +0 -0
  133. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_pkg_update_check.py +0 -0
  134. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_sdk_preinstall.py +0 -0
  135. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_sdk_uninstall.py +0 -0
  136. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_skills_commands.py +0 -0
  137. {sima_cli-2.1.7 → sima_cli-2.1.8}/tests/unit/test_skills_manager.py +0 -0
  138. {sima_cli-2.1.7 → 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.7
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.7"
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.7"
2
+ __version__ = "2.1.8"
@@ -30,7 +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 ENV_BASE_URLS, install_vulcan_package
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
+ )
34
40
 
35
41
  def _configure_stdio_errors() -> None:
36
42
  for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
@@ -505,21 +511,29 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
505
511
  @click.argument("component", required=False)
506
512
  @click.option("-v", "--version", help="SDK version (required for SDK-dependent components unless --metadata is provided)")
507
513
  @click.option("-m", "--mirror", help="URL to a metadata.json file for generic installation")
508
- @click.option("-t", "--tag", help="Tag of the package. With --vulcan, metadata variant type such as minimum.")
509
- @click.option("--vulcan", "use_vulcan", is_flag=True, help="Install from Vulcan artifacts using the Vulcan package resolver.")
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
510
523
  @click.option(
511
524
  "--env",
512
525
  "vulcan_environment",
513
- type=click.Choice(sorted(ENV_BASE_URLS), case_sensitive=False),
526
+ metavar=ENV_METAVAR,
527
+ callback=_normalize_environment_option,
514
528
  default=None,
515
- help="Vulcan artifact environment. Used with --vulcan. Defaults to production.",
529
+ help="Neat artifact environment. Used with --neat or --vulcan. Defaults to production.",
516
530
  )
517
531
  @click.option(
518
532
  "--base-url",
519
533
  "vulcan_base_url",
520
534
  default=None,
521
- envvar="SIMA_VULCAN_BASE_URL",
522
- help="Override the Vulcan artifact base URL. Used with --vulcan.",
535
+ envvar="SIMA_NEAT_BASE_URL",
536
+ help="Override the Neat artifact base URL. Used with --neat or --vulcan.",
523
537
  )
524
538
  @click.option(
525
539
  "-d",
@@ -527,9 +541,9 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
527
541
  default=".",
528
542
  show_default=True,
529
543
  type=click.Path(file_okay=False, dir_okay=True, path_type=str),
530
- help="Directory where Vulcan package resources are downloaded and installed. Used with --vulcan.",
544
+ help="Directory where Neat package resources are downloaded and installed. Used with --neat or --vulcan.",
531
545
  )
532
- @click.option("--json", "json_output", is_flag=True, help="With --vulcan, print resolved metadata URL and exit.")
546
+ @click.option("--json", "json_output", is_flag=True, help="With --neat or --vulcan, print resolved metadata URL and exit.")
533
547
  @click.option(
534
548
  "-f",
535
549
  "--force",
@@ -538,7 +552,21 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
538
552
  help="Force installation even if compatibility checks fail.",
539
553
  )
540
554
  @click.pass_context
541
- def install_cmd(ctx, component, version, mirror, tag, use_vulcan, vulcan_environment, vulcan_base_url, install_dir, json_output, 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
+ ):
542
570
  """
543
571
  Install SiMa packages.
544
572
 
@@ -573,15 +601,18 @@ def install_cmd(ctx, component, version, mirror, tag, use_vulcan, vulcan_environ
573
601
  """
574
602
  internal = ctx.obj.get("internal", False)
575
603
 
576
- if use_vulcan:
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.")
577
608
  if mirror:
578
- raise click.ClickException("--mirror cannot be used with --vulcan.")
609
+ raise click.ClickException("--mirror cannot be used with --neat.")
579
610
  if not component:
580
- raise click.ClickException("You must specify a Vulcan target when using --vulcan.")
611
+ raise click.ClickException("You must specify a Neat target when using --neat.")
581
612
  install_vulcan_package(
582
613
  target=component,
583
- environment=vulcan_environment or "production",
584
- base_url=vulcan_base_url,
614
+ environment=_resolve_environment(vulcan_environment, environment_flag),
615
+ base_url=vulcan_base_url or os.getenv("SIMA_VULCAN_BASE_URL"),
585
616
  package_type=tag,
586
617
  install_dir=install_dir,
587
618
  force=force,
@@ -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.
@@ -1222,8 +1410,10 @@ def install_from_metadata(metadata_url: str, internal: bool, install_dir: str =
1222
1410
  if _is_platform_compatible(metadata, force) or force:
1223
1411
  local_paths = _download_assets(metadata, metadata_url, install_dir, internal, tag=tag)
1224
1412
  if len(local_paths) > 0:
1413
+ _mark_install_script_executable(metadata, install_dir)
1225
1414
  _combine_multipart_files(install_dir, local_paths=local_paths)
1226
1415
  _extract_archives_in_folder(install_dir, local_paths)
1416
+ _mark_install_script_executable(metadata, install_dir)
1227
1417
  _run_installation_script(metadata=metadata, extract_path=install_dir)
1228
1418
 
1229
1419
  except Exception as e:
@@ -242,6 +242,7 @@ def build_metadata(
242
242
  install_script: str = "",
243
243
  selectables: Optional[str] = None,
244
244
  exclude: Optional[Sequence[str]] = None,
245
+ download_compatible_files_only: bool = False,
245
246
  ) -> Dict:
246
247
  artifacts_folder = artifacts_folder.expanduser().resolve()
247
248
  if not artifacts_folder.is_dir():
@@ -266,7 +267,7 @@ def build_metadata(
266
267
  resolved_description = github_repo_description(artifacts_folder)
267
268
 
268
269
  size_text = _format_size(total_size)
269
- return {
270
+ metadata = {
270
271
  "name": name or default_package_name(artifacts_folder),
271
272
  "version": version or default_version(artifacts_folder),
272
273
  "release": "",
@@ -284,6 +285,9 @@ def build_metadata(
284
285
  "post-message": DEFAULT_POST_MESSAGE,
285
286
  },
286
287
  }
288
+ if download_compatible_files_only:
289
+ metadata["download-compatible-files-only"] = True
290
+ return metadata
287
291
 
288
292
 
289
293
  def metadata_filename(variant: Optional[str] = None) -> str:
@@ -103,7 +103,23 @@ def show_metadata(name, version):
103
103
  "--variant",
104
104
  help="Optional metadata variant name. Writes metadata-<variant>.json instead of metadata.json.",
105
105
  )
106
- def build_package_metadata(artifacts_folder, name, version, description, install_script, selectables, exclude, variant):
106
+ @click.option(
107
+ "--download-compatible-files-only",
108
+ is_flag=True,
109
+ default=False,
110
+ help="Add download-compatible-files-only so installers download only wheel files compatible with the current platform.",
111
+ )
112
+ def build_package_metadata(
113
+ artifacts_folder,
114
+ name,
115
+ version,
116
+ description,
117
+ install_script,
118
+ selectables,
119
+ exclude,
120
+ variant,
121
+ download_compatible_files_only,
122
+ ):
107
123
  """
108
124
  Generate metadata.json, or metadata-<variant>.json, for sima-cli package installation.
109
125
  """
@@ -116,6 +132,7 @@ def build_package_metadata(artifacts_folder, name, version, description, install
116
132
  install_script=install_script,
117
133
  selectables=selectables,
118
134
  exclude=exclude,
135
+ download_compatible_files_only=download_compatible_files_only,
119
136
  )
120
137
  output_path = write_metadata(artifacts_folder, metadata, variant=variant)
121
138
  except ValueError as exc:
@@ -43,6 +43,8 @@ from sima_cli.sdk.utils import (
43
43
  select_containers,
44
44
  )
45
45
 
46
+ LINUX_NEAT_EXPORTS_PATH = Path("/etc/exports.d/neat-sdk.exports")
47
+
46
48
  # ─────────────────────────────────────────────
47
49
  # Entrypoint for setup/start
48
50
  # ─────────────────────────────────────────────
@@ -323,6 +325,7 @@ class ExistingNfsExport:
323
325
  local_export_path: str
324
326
  client: str
325
327
  client_allowed: bool
328
+ managed_by_sima: bool = False
326
329
 
327
330
 
328
331
  @dataclass(frozen=True)
@@ -330,6 +333,7 @@ class ParsedNfsExport:
330
333
  path: Path
331
334
  client: str
332
335
  options: Tuple[str, ...]
336
+ source: Optional[Path] = None
333
337
 
334
338
 
335
339
  def _parse_export_line(line: str) -> List[ParsedNfsExport]:
@@ -373,10 +377,16 @@ def _read_linux_exports() -> List[ParsedNfsExport]:
373
377
  logical_line += line[:-1] + " "
374
378
  continue
375
379
  logical_line += line
376
- exports.extend(_parse_export_line(logical_line))
380
+ exports.extend(
381
+ ParsedNfsExport(export.path, export.client, export.options, source=path)
382
+ for export in _parse_export_line(logical_line)
383
+ )
377
384
  logical_line = ""
378
385
  if logical_line:
379
- exports.extend(_parse_export_line(logical_line))
386
+ exports.extend(
387
+ ParsedNfsExport(export.path, export.client, export.options, source=path)
388
+ for export in _parse_export_line(logical_line)
389
+ )
380
390
  except OSError:
381
391
  continue
382
392
  return exports
@@ -416,6 +426,12 @@ def _join_nfs_path(base: str, relative: Path) -> str:
416
426
  return f"{base.rstrip('/')}/{relative_text}"
417
427
 
418
428
 
429
+ def _is_sima_managed_linux_export(export: ParsedNfsExport) -> bool:
430
+ if export.source is None:
431
+ return False
432
+ return export.source == LINUX_NEAT_EXPORTS_PATH
433
+
434
+
419
435
  def _resolve_client_visible_export_path(workspace: Path, matching_export: ParsedNfsExport, exports: List[ParsedNfsExport]) -> str:
420
436
  workspace_path = workspace.resolve()
421
437
 
@@ -452,12 +468,14 @@ def _detect_existing_linux_nfs_export(workspace: Path, devkit_ip: str, host_ip:
452
468
 
453
469
  allowed_exports = [export for export in exports if _export_allows_client(export.client, devkit_ip)]
454
470
  matching_export = max(allowed_exports or exports, key=lambda export: len(str(export.path.resolve())))
471
+ managed_by_sima = not allowed_exports and all(_is_sima_managed_linux_export(export) for export in exports)
455
472
  return ExistingNfsExport(
456
473
  server=host_ip,
457
474
  export_path=_resolve_client_visible_export_path(workspace_path, matching_export, exports),
458
475
  local_export_path=str(matching_export.path.resolve()),
459
476
  client=matching_export.client,
460
477
  client_allowed=bool(allowed_exports),
478
+ managed_by_sima=managed_by_sima,
461
479
  )
462
480
 
463
481
 
@@ -551,10 +569,29 @@ def _setup_devkit_share(devkit_ip: str, workspace: str, selected_images: List[st
551
569
  )
552
570
  )
553
571
  if not existing_export.client_allowed:
572
+ if existing_export.managed_by_sima:
573
+ print(
574
+ "ℹ️ Existing sima-cli-managed NFS export allows {}, updating it for DevKit {}.".format(
575
+ existing_export.client,
576
+ devkit_ip,
577
+ )
578
+ )
579
+ _print_devkit_nfs_banner(workspace, devkit_ip, host_os)
580
+ _configure_nfs_export(host_dir, devkit_ip, host_os, host_ip)
581
+ configure_linux_shared_devkit_network(devkit_ip)
582
+ print("✅ Host NFS export configured for workspace {} -> {}".format(workspace, devkit_ip))
583
+ return {
584
+ "devkit_ip": devkit_ip,
585
+ "host_ip": host_ip,
586
+ "workspace": workspace,
587
+ "host_platform": host_os,
588
+ "bootstrap_interactive": not noninteractive,
589
+ "noninteractive": noninteractive,
590
+ }
554
591
  raise RuntimeError(
555
- "Workspace is under an existing admin-managed NFS export, but DevKit {} is not allowed "
592
+ "Workspace is under an existing unmanaged NFS export, but DevKit {} is not allowed "
556
593
  "by the export client '{}'. Ask an admin to add an export entry that covers {} for the "
557
- "DevKit IP/subnet, then rerun setup. sima-cli will not try to modify this admin-managed "
594
+ "DevKit IP/subnet, then rerun setup. sima-cli will not try to modify this unmanaged "
558
595
  "export without permission.".format(
559
596
  devkit_ip,
560
597
  existing_export.client,
@@ -562,7 +599,7 @@ def _setup_devkit_share(devkit_ip: str, workspace: str, selected_images: List[st
562
599
  )
563
600
  )
564
601
  print(
565
- "ℹ️ Reusing admin-managed NFS export for DevKit sync: {}:{}.".format(
602
+ "ℹ️ Reusing existing NFS export for DevKit sync: {}:{}.".format(
566
603
  existing_export.server,
567
604
  existing_export.export_path,
568
605
  )
@@ -916,6 +916,8 @@ def install_neat_playbooks(sdk_container_name: str, login_name: str) -> None:
916
916
  "exec",
917
917
  "-u",
918
918
  login_name,
919
+ "-e",
920
+ "SIMA_CLI_CHECK_FOR_UPDATE=0",
919
921
  sdk_container_name,
920
922
  "bash",
921
923
  "-lc",