sima-cli 2.1.5__tar.gz → 2.1.7__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 (137) hide show
  1. {sima_cli-2.1.5/sima_cli.egg-info → sima_cli-2.1.7}/PKG-INFO +34 -2
  2. {sima_cli-2.1.5 → sima_cli-2.1.7}/README.md +33 -1
  3. {sima_cli-2.1.5 → sima_cli-2.1.7}/pyproject.toml +1 -1
  4. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/__version__.py +1 -1
  5. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/cli.py +52 -5
  6. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/metadata_installer.py +4 -0
  7. sima_cli-2.1.7/sima_cli/install/package_builder.py +304 -0
  8. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/registry.py +49 -0
  9. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/commands.py +15 -2
  10. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/install.py +201 -7
  11. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/neat.py +65 -16
  12. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/preinstall.py +10 -3
  13. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/utils.py +32 -6
  14. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/elxr.py +116 -10
  15. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/netboot.py +126 -24
  16. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/updater.py +11 -16
  17. sima_cli-2.1.7/sima_cli/vulcan/__init__.py +3 -0
  18. sima_cli-2.1.7/sima_cli/vulcan/artifacts.py +452 -0
  19. sima_cli-2.1.7/sima_cli/vulcan/commands.py +226 -0
  20. {sima_cli-2.1.5 → sima_cli-2.1.7/sima_cli.egg-info}/PKG-INFO +34 -2
  21. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli.egg-info/SOURCES.txt +9 -1
  22. sima_cli-2.1.7/tests/unit/test_elxr_update.py +408 -0
  23. sima_cli-2.1.7/tests/unit/test_firmware.py +60 -0
  24. sima_cli-2.1.7/tests/unit/test_metadata_installer.py +21 -0
  25. sima_cli-2.1.7/tests/unit/test_netboot.py +98 -0
  26. sima_cli-2.1.7/tests/unit/test_package_builder.py +314 -0
  27. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_sdk_image_detection.py +377 -5
  28. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_sdk_preinstall.py +14 -0
  29. sima_cli-2.1.7/tests/unit/test_vulcan.py +445 -0
  30. sima_cli-2.1.5/tests/unit/test_elxr_update.py +0 -110
  31. sima_cli-2.1.5/tests/unit/test_firmware.py +0 -0
  32. {sima_cli-2.1.5 → sima_cli-2.1.7}/LICENSE +0 -0
  33. {sima_cli-2.1.5 → sima_cli-2.1.7}/MANIFEST.in +0 -0
  34. {sima_cli-2.1.5 → sima_cli-2.1.7}/requirements.txt +0 -0
  35. {sima_cli-2.1.5 → sima_cli-2.1.7}/setup.cfg +0 -0
  36. {sima_cli-2.1.5 → sima_cli-2.1.7}/setup.py +0 -0
  37. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/__init__.py +0 -0
  38. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/__main__.py +0 -0
  39. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/app_zoo/__init__.py +0 -0
  40. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/app_zoo/app.py +0 -0
  41. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/app_zoo/commands.py +0 -0
  42. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/auth/__init__.py +0 -0
  43. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/auth/auth0.py +0 -0
  44. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/auth/devportal.py +0 -0
  45. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/auth/login.py +0 -0
  46. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/auth/oauth.py +0 -0
  47. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/data/__init__.py +0 -0
  48. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/data/resources_internal.yaml +0 -0
  49. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/data/resources_public.yaml +0 -0
  50. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/deploy_only/__init__.py +0 -0
  51. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/deploy_only/device/__init__.py +0 -0
  52. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/deploy_only/device/commands.py +0 -0
  53. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  54. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/commands.py +0 -0
  55. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/discover/__init__.py +0 -0
  56. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/discover/discover.py +0 -0
  57. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/discover/linuxll.py +0 -0
  58. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/download/__init__.py +0 -0
  59. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/download/downloader.py +0 -0
  60. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/__init__.py +0 -0
  61. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/github_assets.py +0 -0
  62. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/hostdriver.py +0 -0
  63. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/metadata_info.py +0 -0
  64. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/metadata_validator.py +0 -0
  65. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/optiview.py +0 -0
  66. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/install/palette.py +0 -0
  67. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/mla/__init__.py +0 -0
  68. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/mla/meminfo.py +0 -0
  69. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/model_zoo/__init__.py +0 -0
  70. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/model_zoo/model.py +0 -0
  71. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/network/__init__.py +0 -0
  72. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/network/network.py +0 -0
  73. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/playbooks/__init__.py +0 -0
  74. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/playbooks/commands.py +0 -0
  75. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/playbooks/manager.py +0 -0
  76. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/__init__.py +0 -0
  77. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/cmdexec.py +0 -0
  78. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/config.py +0 -0
  79. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/linux_shared_network.py +0 -0
  80. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/requirements.json +0 -0
  81. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/script.py +0 -0
  82. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/stop.py +0 -0
  83. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/sdk/uninstall.py +0 -0
  84. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/serial/__init__.py +0 -0
  85. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/serial/serial.py +0 -0
  86. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/storage/__init__.py +0 -0
  87. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/storage/nvme.py +0 -0
  88. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/storage/sdcard.py +0 -0
  89. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/__init__.py +0 -0
  90. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/bmaptool.py +0 -0
  91. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/bootimg.py +0 -0
  92. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/cleanlog.py +0 -0
  93. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/local.py +0 -0
  94. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/query.py +0 -0
  95. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/update/remote.py +0 -0
  96. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/upgrade/__init__.py +0 -0
  97. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/upgrade/selfupdate.py +0 -0
  98. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/__init__.py +0 -0
  99. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/api_common.py +0 -0
  100. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/artifactory.py +0 -0
  101. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/common.py +0 -0
  102. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/config.py +0 -0
  103. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/config_loader.py +0 -0
  104. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/container_registries.py +0 -0
  105. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/device_api.py +0 -0
  106. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/disk.py +0 -0
  107. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/docker.py +0 -0
  108. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/env.py +0 -0
  109. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/errors.py +0 -0
  110. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/mpk_api.py +0 -0
  111. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/net.py +0 -0
  112. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/network.py +0 -0
  113. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/pcie.py +0 -0
  114. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/pkg_update_check.py +0 -0
  115. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/serializers.py +0 -0
  116. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/services.py +0 -0
  117. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli/utils/tag.py +0 -0
  118. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli.egg-info/dependency_links.txt +0 -0
  119. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli.egg-info/entry_points.txt +0 -0
  120. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli.egg-info/requires.txt +0 -0
  121. {sima_cli-2.1.5 → sima_cli-2.1.7}/sima_cli.egg-info/top_level.txt +0 -0
  122. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/__init__.py +0 -0
  123. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/e2e/__init__.py +0 -0
  124. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/__init__.py +0 -0
  125. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_app_zoo.py +0 -0
  126. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_auth.py +0 -0
  127. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_cli.py +0 -0
  128. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_cli_stdio.py +0 -0
  129. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_download.py +0 -0
  130. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_install_stub.py +0 -0
  131. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_model_zoo.py +0 -0
  132. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_pkg_update_check.py +0 -0
  133. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_sdk_uninstall.py +0 -0
  134. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_selfupdate.py +0 -0
  135. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_skills_commands.py +0 -0
  136. {sima_cli-2.1.5 → sima_cli-2.1.7}/tests/unit/test_skills_manager.py +0 -0
  137. {sima_cli-2.1.5 → sima_cli-2.1.7}/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.5
3
+ Version: 2.1.7
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
@@ -152,6 +152,34 @@ sima-cli download <URL> [-d DEST]
152
152
 
153
153
  ---
154
154
 
155
+ ## 🔥 Vulcan Artifacts
156
+
157
+ ```bash
158
+ sima-cli vulcan download --env production core main
159
+ ```
160
+
161
+ - Downloads tested artifacts from Vulcan artifact hosting.
162
+ - Environment URLs:
163
+ - `dev`: `https://artifacts.neat.paconsultings.com`
164
+ - `staging`: `https://artifacts.stg.neat.sima.ai`
165
+ - `production`: `https://artifacts.neat.sima.ai`
166
+ - `staging` and `production` are not yet available for Vulcan downloads; use `--env dev` for now.
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]`
170
+ - If `REPO` is omitted, the CLI prompts for a repository.
171
+ - If `BRANCH_OR_TAG` is omitted, the CLI downloads `branches.json` and prompts for a branch.
172
+ - For automation, pass both values and add `--json` for structured output.
173
+ - Each download reads `latest.tag`, fetches `manifest.json`, downloads manifest-listed artifacts, and verifies size and SHA256 values when present.
174
+
175
+ Example:
176
+
177
+ ```bash
178
+ sima-cli vulcan --env dev download internals develop --output ./artifacts --json
179
+ ```
180
+
181
+ ---
182
+
155
183
  ## 🔧 Firmware Update
156
184
 
157
185
  ```bash
@@ -379,10 +407,14 @@ sima-cli packages show <PACKAGE_NAME> [--version VERSION]
379
407
  ### Setup SDK
380
408
 
381
409
  ```bash
382
- sima-cli sdk setup [--noninteractive] [-y]
410
+ sima-cli sdk setup [--noninteractive] [-y] [--workspace PATH] [--no-model-sdk] [--no-insight] [--minimal]
383
411
  ```
384
412
 
385
413
  - Initialize SDK environment and select components to start.
414
+ - Use `--workspace PATH` to mount a host directory other than `~/workspace` into SDK containers.
415
+ - Use `--no-model-sdk` to skip Model SDK extension setup.
416
+ - Use `--no-insight` to start Neat SDK without Insight UI/video/WebRTC port mappings.
417
+ - Use `--minimal` for CI compilation jobs. It skips optional Neat SDK setup extras, including Insight setup, installing `sima-cli`, Model SDK extensions, and coding agent playbooks inside the container.
386
418
 
387
419
  ### Start SDK Containers
388
420
 
@@ -118,6 +118,34 @@ sima-cli download <URL> [-d DEST]
118
118
 
119
119
  ---
120
120
 
121
+ ## 🔥 Vulcan Artifacts
122
+
123
+ ```bash
124
+ sima-cli vulcan download --env production core main
125
+ ```
126
+
127
+ - Downloads tested artifacts from Vulcan artifact hosting.
128
+ - Environment URLs:
129
+ - `dev`: `https://artifacts.neat.paconsultings.com`
130
+ - `staging`: `https://artifacts.stg.neat.sima.ai`
131
+ - `production`: `https://artifacts.neat.sima.ai`
132
+ - `staging` and `production` are not yet available for Vulcan downloads; use `--env dev` for now.
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]`
136
+ - If `REPO` is omitted, the CLI prompts for a repository.
137
+ - If `BRANCH_OR_TAG` is omitted, the CLI downloads `branches.json` and prompts for a branch.
138
+ - For automation, pass both values and add `--json` for structured output.
139
+ - Each download reads `latest.tag`, fetches `manifest.json`, downloads manifest-listed artifacts, and verifies size and SHA256 values when present.
140
+
141
+ Example:
142
+
143
+ ```bash
144
+ sima-cli vulcan --env dev download internals develop --output ./artifacts --json
145
+ ```
146
+
147
+ ---
148
+
121
149
  ## 🔧 Firmware Update
122
150
 
123
151
  ```bash
@@ -345,10 +373,14 @@ sima-cli packages show <PACKAGE_NAME> [--version VERSION]
345
373
  ### Setup SDK
346
374
 
347
375
  ```bash
348
- sima-cli sdk setup [--noninteractive] [-y]
376
+ sima-cli sdk setup [--noninteractive] [-y] [--workspace PATH] [--no-model-sdk] [--no-insight] [--minimal]
349
377
  ```
350
378
 
351
379
  - Initialize SDK environment and select components to start.
380
+ - Use `--workspace PATH` to mount a host directory other than `~/workspace` into SDK containers.
381
+ - Use `--no-model-sdk` to skip Model SDK extension setup.
382
+ - Use `--no-insight` to start Neat SDK without Insight UI/video/WebRTC port mappings.
383
+ - Use `--minimal` for CI compilation jobs. It skips optional Neat SDK setup extras, including Insight setup, installing `sima-cli`, Model SDK extensions, and coding agent playbooks inside the container.
352
384
 
353
385
  ### Start SDK Containers
354
386
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.5"
7
+ version = "2.1.7"
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.5"
2
+ __version__ = "2.1.7"
@@ -29,6 +29,8 @@ from sima_cli.app_zoo.commands import register_appzoo_commands
29
29
  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
+ from sima_cli.vulcan import register_vulcan_commands
33
+ from sima_cli.vulcan.commands import ENV_BASE_URLS, install_vulcan_package
32
34
 
33
35
  def _configure_stdio_errors() -> None:
34
36
  for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
@@ -89,6 +91,7 @@ def main(ctx, internal):
89
91
  # ----------------------
90
92
  register_sdk_commands(main)
91
93
  register_playbook_commands(main)
94
+ register_vulcan_commands(main)
92
95
 
93
96
 
94
97
  # ----------------------
@@ -502,7 +505,31 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
502
505
  @click.argument("component", required=False)
503
506
  @click.option("-v", "--version", help="SDK version (required for SDK-dependent components unless --metadata is provided)")
504
507
  @click.option("-m", "--mirror", help="URL to a metadata.json file for generic installation")
505
- @click.option("-t", "--tag", help="Tag of the package (optional)")
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.")
510
+ @click.option(
511
+ "--env",
512
+ "vulcan_environment",
513
+ type=click.Choice(sorted(ENV_BASE_URLS), case_sensitive=False),
514
+ default=None,
515
+ help="Vulcan artifact environment. Used with --vulcan. Defaults to production.",
516
+ )
517
+ @click.option(
518
+ "--base-url",
519
+ "vulcan_base_url",
520
+ default=None,
521
+ envvar="SIMA_VULCAN_BASE_URL",
522
+ help="Override the Vulcan artifact base URL. Used with --vulcan.",
523
+ )
524
+ @click.option(
525
+ "-d",
526
+ "--install-dir",
527
+ default=".",
528
+ show_default=True,
529
+ 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.",
531
+ )
532
+ @click.option("--json", "json_output", is_flag=True, help="With --vulcan, print resolved metadata URL and exit.")
506
533
  @click.option(
507
534
  "-f",
508
535
  "--force",
@@ -511,7 +538,7 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
511
538
  help="Force installation even if compatibility checks fail.",
512
539
  )
513
540
  @click.pass_context
514
- def install_cmd(ctx, component, version, mirror, tag, force):
541
+ def install_cmd(ctx, component, version, mirror, tag, use_vulcan, vulcan_environment, vulcan_base_url, install_dir, json_output, force):
515
542
  """
516
543
  Install SiMa packages.
517
544
 
@@ -546,12 +573,29 @@ def install_cmd(ctx, component, version, mirror, tag, force):
546
573
  """
547
574
  internal = ctx.obj.get("internal", False)
548
575
 
576
+ if use_vulcan:
577
+ if mirror:
578
+ raise click.ClickException("--mirror cannot be used with --vulcan.")
579
+ if not component:
580
+ raise click.ClickException("You must specify a Vulcan target when using --vulcan.")
581
+ install_vulcan_package(
582
+ target=component,
583
+ environment=vulcan_environment or "production",
584
+ base_url=vulcan_base_url,
585
+ package_type=tag,
586
+ install_dir=install_dir,
587
+ force=force,
588
+ json_output=json_output,
589
+ )
590
+ return None
591
+
549
592
  # Metadata-based installation path
550
593
  if mirror:
551
594
  if component:
552
595
  click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
553
596
  click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
554
- return install_from_metadata(metadata_url=mirror, internal=internal, force=force)
597
+ install_from_metadata(metadata_url=mirror, internal=internal, force=force)
598
+ return None
555
599
 
556
600
  # No component and no metadata: error
557
601
  if not component:
@@ -560,11 +604,13 @@ def install_cmd(ctx, component, version, mirror, tag, force):
560
604
 
561
605
  # if user specified gh: as component, treat it the same as -m
562
606
  if component.startswith("gh:"):
563
- return install_from_metadata(metadata_url=component, internal=False, force=force)
607
+ install_from_metadata(metadata_url=component, internal=False, force=force)
608
+ return None
564
609
 
565
610
  # if the user specified cr: or ghcr: as component, install from container registry
566
611
  if component.startswith("cr:") or component.startswith("ghcr:"):
567
- return install_from_cr(resource_spec=component, internal=internal)
612
+ install_from_cr(resource_spec=component, internal=internal)
613
+ return None
568
614
 
569
615
  version = resolve_version(version)
570
616
 
@@ -599,6 +645,7 @@ def install_cmd(ctx, component, version, mirror, tag, force):
599
645
  ctx.exit(1)
600
646
 
601
647
  click.echo("✅ Installation complete.")
648
+ return None
602
649
 
603
650
 
604
651
  # ----------------------
@@ -936,6 +936,10 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
936
936
  platforms = metadata.get("platforms", [])
937
937
  board_ver, _ = get_sima_build_version()
938
938
 
939
+ if not platforms:
940
+ click.echo("ℹ️ No platform restrictions specified; treating package as compatible with all platforms.")
941
+ return True
942
+
939
943
  # Detect current OS and version
940
944
  os_name = platform.system().lower() # 'darwin', 'windows', 'linux'
941
945
  os_version = "Unknown"
@@ -0,0 +1,304 @@
1
+ import datetime
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Sequence, Tuple
9
+
10
+ import requests
11
+
12
+
13
+ METADATA_FILENAME = "metadata.json"
14
+ DEFAULT_POST_MESSAGE = "[bold]Package installed successfully.[/bold]\n"
15
+
16
+
17
+ def _run_git(args: List[str], cwd: Path) -> Optional[str]:
18
+ try:
19
+ result = subprocess.run(
20
+ ["git"] + args,
21
+ cwd=str(cwd),
22
+ check=True,
23
+ stdout=subprocess.PIPE,
24
+ stderr=subprocess.DEVNULL,
25
+ text=True,
26
+ )
27
+ except (OSError, subprocess.CalledProcessError):
28
+ return None
29
+ return result.stdout.strip()
30
+
31
+
32
+ def _git_root(search_dir: Path) -> Optional[Path]:
33
+ output = _run_git(["rev-parse", "--show-toplevel"], search_dir)
34
+ if not output:
35
+ return None
36
+ return Path(output).resolve()
37
+
38
+
39
+ def _github_repo_from_remote(remote_url: str) -> Optional[Tuple[str, str]]:
40
+ value = remote_url.strip()
41
+ if value.endswith(".git"):
42
+ value = value[:-4]
43
+
44
+ if value.startswith("git@github.com:"):
45
+ value = value[len("git@github.com:"):]
46
+ elif value.startswith("https://github.com/"):
47
+ value = value[len("https://github.com/"):]
48
+ elif value.startswith("http://github.com/"):
49
+ value = value[len("http://github.com/"):]
50
+ else:
51
+ return None
52
+
53
+ parts = value.split("/")
54
+ if len(parts) != 2 or not parts[0] or not parts[1]:
55
+ return None
56
+ return parts[0], parts[1]
57
+
58
+
59
+ def resolve_git_context(artifacts_folder: Path) -> Tuple[Optional[Path], Optional[Tuple[str, str]]]:
60
+ for search_dir in (Path.cwd(), artifacts_folder):
61
+ root = _git_root(search_dir)
62
+ if not root:
63
+ continue
64
+ remote = _run_git(["remote", "get-url", "origin"], root) or ""
65
+ return root, _github_repo_from_remote(remote)
66
+ return None, None
67
+
68
+
69
+ def default_package_name(artifacts_folder: Path) -> str:
70
+ git_root, github_repo = resolve_git_context(artifacts_folder)
71
+ if github_repo:
72
+ owner, repo = github_repo
73
+ return "gh:{}/{}".format(owner, repo)
74
+ if git_root:
75
+ return git_root.name
76
+ return Path.cwd().name
77
+
78
+
79
+ def default_version(artifacts_folder: Path) -> str:
80
+ git_root, _github_repo = resolve_git_context(artifacts_folder)
81
+ if git_root:
82
+ tag = _run_git(["describe", "--tags", "--exact-match"], git_root)
83
+ if tag:
84
+ return tag
85
+ commit = _run_git(["rev-parse", "--short", "HEAD"], git_root)
86
+ if commit:
87
+ return commit
88
+ return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
89
+
90
+
91
+ def github_repo_description(artifacts_folder: Path) -> str:
92
+ _git_root, github_repo = resolve_git_context(artifacts_folder)
93
+ if not github_repo:
94
+ return ""
95
+
96
+ owner, repo = github_repo
97
+ headers = {"Accept": "application/vnd.github+json"}
98
+ token = os.getenv("GITHUB_TOKEN")
99
+ if token:
100
+ headers["Authorization"] = "Bearer {}".format(token)
101
+
102
+ try:
103
+ response = requests.get(
104
+ "https://api.github.com/repos/{}/{}".format(owner, repo),
105
+ headers=headers,
106
+ timeout=5,
107
+ )
108
+ response.raise_for_status()
109
+ description = response.json().get("description")
110
+ except Exception:
111
+ return ""
112
+ return description if isinstance(description, str) else ""
113
+
114
+
115
+ def _sha256_file(path: Path) -> str:
116
+ hasher = hashlib.sha256()
117
+ with path.open("rb") as file_obj:
118
+ for chunk in iter(lambda: file_obj.read(1024 * 1024), b""):
119
+ hasher.update(chunk)
120
+ return hasher.hexdigest()
121
+
122
+
123
+ def _format_size(total_bytes: int) -> str:
124
+ units = (("GB", 1024 ** 3), ("MB", 1024 ** 2), ("KB", 1024))
125
+ if total_bytes <= 0:
126
+ return "0KB"
127
+ for suffix, factor in units:
128
+ if total_bytes >= factor:
129
+ value = total_bytes / factor
130
+ text = "{:.1f}".format(value).rstrip("0").rstrip(".")
131
+ return "{}{}".format(text, suffix)
132
+ return "1KB"
133
+
134
+
135
+ def _matches_exclude_pattern(resource: str, patterns: Sequence[str]) -> bool:
136
+ filename = Path(resource).name
137
+ return any(pattern and (pattern in resource or pattern in filename) for pattern in patterns)
138
+
139
+
140
+ def collect_artifact_resources(
141
+ artifacts_folder: Path,
142
+ exclude_patterns: Optional[Sequence[str]] = None,
143
+ ) -> Tuple[List[str], Dict[str, Path], int]:
144
+ resources = []
145
+ resource_paths = {}
146
+ total_size = 0
147
+ normalized_excludes = [pattern.strip() for pattern in (exclude_patterns or []) if pattern.strip()]
148
+
149
+ for path in sorted(artifacts_folder.rglob("*")):
150
+ if not path.is_file():
151
+ continue
152
+ rel = path.relative_to(artifacts_folder).as_posix()
153
+ if rel == METADATA_FILENAME or re.fullmatch(r"metadata-[A-Za-z0-9_.-]+\.json", rel):
154
+ continue
155
+ if _matches_exclude_pattern(rel, normalized_excludes):
156
+ continue
157
+ resources.append(rel)
158
+ resource_paths[rel] = path
159
+ total_size += path.stat().st_size
160
+
161
+ return resources, resource_paths, total_size
162
+
163
+
164
+ def parse_selectables(selectables: Optional[str]) -> List[Tuple[str, str]]:
165
+ if not selectables:
166
+ return []
167
+
168
+ parsed = []
169
+ for raw_item in selectables.split(";"):
170
+ item = raw_item.strip()
171
+ if not item:
172
+ continue
173
+ if ":" not in item:
174
+ raise ValueError("selectables entries must use 'name:file' format")
175
+ name, resource = item.split(":", 1)
176
+ name = name.strip()
177
+ resource = resource.strip()
178
+ if not name or not resource:
179
+ raise ValueError("selectables entries must include both name and file")
180
+ parsed.append((name, resource))
181
+ return parsed
182
+
183
+
184
+ def build_selectable_resources(
185
+ selectable_specs: List[Tuple[str, str]],
186
+ resource_paths: Dict[str, Path],
187
+ ) -> List[Dict[str, str]]:
188
+ selectable_resources = []
189
+ seen_resources = set()
190
+
191
+ for name, resource in selectable_specs:
192
+ if resource not in resource_paths:
193
+ raise ValueError("selectable resource is not in artifacts-folder: {}".format(resource))
194
+ if resource in seen_resources:
195
+ raise ValueError("duplicate selectable resource: {}".format(resource))
196
+ seen_resources.add(resource)
197
+ selectable_resources.append({
198
+ "name": name,
199
+ "url": "",
200
+ "resource": resource,
201
+ "checksum": _sha256_file(resource_paths[resource]),
202
+ })
203
+
204
+ return selectable_resources
205
+
206
+
207
+ def resolve_install_script(artifacts_folder: Path, install_script: str) -> str:
208
+ script = install_script.strip()
209
+ if not script:
210
+ raise ValueError("install-script cannot be empty")
211
+
212
+ candidate_paths = []
213
+ raw_path = Path(script).expanduser()
214
+ if raw_path.is_absolute():
215
+ candidate_paths.append(raw_path)
216
+ else:
217
+ candidate_paths.append(artifacts_folder / raw_path)
218
+ candidate_paths.append(Path.cwd() / raw_path)
219
+
220
+ artifacts_root = artifacts_folder.resolve()
221
+ for candidate in candidate_paths:
222
+ try:
223
+ resolved = candidate.resolve()
224
+ except OSError:
225
+ continue
226
+ if not resolved.is_file():
227
+ continue
228
+ try:
229
+ rel = resolved.relative_to(artifacts_root).as_posix()
230
+ except ValueError:
231
+ continue
232
+ return "./{}".format(rel)
233
+
234
+ return script
235
+
236
+
237
+ def build_metadata(
238
+ artifacts_folder: Path,
239
+ name: Optional[str] = None,
240
+ version: Optional[str] = None,
241
+ description: Optional[str] = None,
242
+ install_script: str = "",
243
+ selectables: Optional[str] = None,
244
+ exclude: Optional[Sequence[str]] = None,
245
+ ) -> Dict:
246
+ artifacts_folder = artifacts_folder.expanduser().resolve()
247
+ if not artifacts_folder.is_dir():
248
+ raise ValueError("artifacts-folder is not a directory: {}".format(artifacts_folder))
249
+
250
+ resources, resource_paths, total_size = collect_artifact_resources(artifacts_folder, exclude_patterns=exclude)
251
+ if not resources:
252
+ raise ValueError("artifacts-folder does not contain any artifact files")
253
+
254
+ selectable_specs = parse_selectables(selectables)
255
+ selectable_resources = build_selectable_resources(selectable_specs, resource_paths)
256
+ selectable_resource_names = {entry["resource"] for entry in selectable_resources}
257
+ resources = [resource for resource in resources if resource not in selectable_resource_names]
258
+ checksums = {
259
+ resource: _sha256_file(path)
260
+ for resource, path in resource_paths.items()
261
+ if resource not in selectable_resource_names
262
+ }
263
+
264
+ resolved_description = description
265
+ if resolved_description is None:
266
+ resolved_description = github_repo_description(artifacts_folder)
267
+
268
+ size_text = _format_size(total_size)
269
+ return {
270
+ "name": name or default_package_name(artifacts_folder),
271
+ "version": version or default_version(artifacts_folder),
272
+ "release": "",
273
+ "description": resolved_description or "",
274
+ "platforms": [],
275
+ "resources": resources,
276
+ "resources-checksum": checksums,
277
+ "selectable-resources": selectable_resources,
278
+ "size": {
279
+ "download": size_text,
280
+ "install": size_text,
281
+ },
282
+ "installation": {
283
+ "script": resolve_install_script(artifacts_folder, install_script),
284
+ "post-message": DEFAULT_POST_MESSAGE,
285
+ },
286
+ }
287
+
288
+
289
+ def metadata_filename(variant: Optional[str] = None) -> str:
290
+ if variant is None:
291
+ return METADATA_FILENAME
292
+
293
+ normalized = variant.strip()
294
+ if not normalized:
295
+ return METADATA_FILENAME
296
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", normalized):
297
+ raise ValueError("variant must contain only letters, numbers, dots, underscores, or hyphens")
298
+ return "metadata-{}.json".format(normalized)
299
+
300
+
301
+ def write_metadata(artifacts_folder: Path, metadata: Dict, variant: Optional[str] = None) -> Path:
302
+ output_path = artifacts_folder.expanduser().resolve() / metadata_filename(variant)
303
+ output_path.write_text(json.dumps(metadata, indent=4) + "\n", encoding="utf-8")
304
+ return output_path
@@ -19,6 +19,7 @@ import json
19
19
  import click
20
20
  import datetime
21
21
  from pathlib import Path
22
+ from sima_cli.install.package_builder import build_metadata, write_metadata
22
23
  from rich.table import Table
23
24
  from rich.console import Console
24
25
  from rich.syntax import Syntax
@@ -74,6 +75,54 @@ def show_metadata(name, version):
74
75
  except ValueError as e:
75
76
  Console().print(f"[red]Error:[/red] {e}")
76
77
 
78
+
79
+ @packages.command("build", help="Build package metadata.json from an artifacts folder.")
80
+ @click.argument(
81
+ "artifacts_folder",
82
+ metavar="ARTIFACTS_FOLDER",
83
+ type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
84
+ )
85
+ @click.option("--name", help="Package name. Defaults to gh:<org>/<repo> for GitHub repos.")
86
+ @click.option("--version", help="Package version. Defaults to exact git tag or short commit hash.")
87
+ @click.option("--description", help="Package description. Defaults to the GitHub repo description when available.")
88
+ @click.option(
89
+ "--install-script",
90
+ required=True,
91
+ help="Install script file inside ARTIFACTS_FOLDER, or a single-line shell command.",
92
+ )
93
+ @click.option(
94
+ "--selectables",
95
+ help="Optional resources in 'name1:file1;name2:file2' format.",
96
+ )
97
+ @click.option(
98
+ "--exclude",
99
+ multiple=True,
100
+ help="Exclude artifact files whose relative path or filename contains this text. May be repeated.",
101
+ )
102
+ @click.option(
103
+ "--variant",
104
+ help="Optional metadata variant name. Writes metadata-<variant>.json instead of metadata.json.",
105
+ )
106
+ def build_package_metadata(artifacts_folder, name, version, description, install_script, selectables, exclude, variant):
107
+ """
108
+ Generate metadata.json, or metadata-<variant>.json, for sima-cli package installation.
109
+ """
110
+ try:
111
+ metadata = build_metadata(
112
+ artifacts_folder=artifacts_folder,
113
+ name=name,
114
+ version=version,
115
+ description=description,
116
+ install_script=install_script,
117
+ selectables=selectables,
118
+ exclude=exclude,
119
+ )
120
+ output_path = write_metadata(artifacts_folder, metadata, variant=variant)
121
+ except ValueError as exc:
122
+ raise click.ClickException(str(exc))
123
+
124
+ click.echo(f"✅ Package metadata written to: {output_path}")
125
+
77
126
  # ------------------------------------------------------------------------------------------------------------
78
127
  # Register the group to main CLI entrypoint, skip this group if it's running inside the SDK container already
79
128
  # ------------------------------------------------------------------------------------------------------------
@@ -38,7 +38,7 @@ console = Console()
38
38
  # ------------------------------------------------------------
39
39
  # Group Definition
40
40
  # ------------------------------------------------------------
41
- @click.group(hidden=True, context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
41
+ @click.group(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
42
42
  @click.option(
43
43
  "-v", "--version",
44
44
  "version_filter",
@@ -173,8 +173,19 @@ def launch_sdk_tool(tool: str, cmd, ctx):
173
173
  is_flag=True,
174
174
  help="Skip Model SDK extension setup. Intended for CI installation tests.",
175
175
  )
176
+ @click.option(
177
+ "--minimal",
178
+ is_flag=True,
179
+ help="Skip optional Neat SDK container extras for CI compilation jobs.",
180
+ )
181
+ @click.option(
182
+ "--workspace",
183
+ type=click.Path(file_okay=False, dir_okay=True),
184
+ default=None,
185
+ help="Host workspace directory to mount into SDK containers instead of ~/workspace.",
186
+ )
176
187
  @click.pass_context
177
- def setup(ctx, yes, noninteractive, devkit, no_insight, no_model_sdk):
188
+ def setup(ctx, yes, noninteractive, devkit, no_insight, no_model_sdk, minimal, workspace):
178
189
  """Initialize SDK environment and select components to start."""
179
190
  devkit_ip = _resolve_devkit_ip(devkit)
180
191
  try:
@@ -184,6 +195,8 @@ def setup(ctx, yes, noninteractive, devkit, no_insight, no_model_sdk):
184
195
  devkit_ip=devkit_ip,
185
196
  no_insight=no_insight,
186
197
  no_model_sdk=no_model_sdk,
198
+ minimal=minimal,
199
+ workspace=workspace,
187
200
  )
188
201
  except subprocess.CalledProcessError as e:
189
202
  raise click.ClickException(f"SDK setup failed while running: {' '.join(e.cmd)}") from e