sima-cli 2.1.8__tar.gz → 2.1.9__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 (139) hide show
  1. {sima_cli-2.1.8/sima_cli.egg-info → sima_cli-2.1.9}/PKG-INFO +3 -3
  2. {sima_cli-2.1.8 → sima_cli-2.1.9}/README.md +1 -1
  3. {sima_cli-2.1.8 → sima_cli-2.1.9}/pyproject.toml +2 -2
  4. {sima_cli-2.1.8 → sima_cli-2.1.9}/requirements.txt +1 -1
  5. {sima_cli-2.1.8 → sima_cli-2.1.9}/setup.py +1 -1
  6. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/__version__.py +1 -1
  7. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/auth/auth0.py +150 -4
  8. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/auth/devportal.py +73 -40
  9. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/metadata_installer.py +4 -4
  10. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/utils.py +35 -19
  11. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/elxr.py +213 -34
  12. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/upgrade/selfupdate.py +2 -1
  13. sima_cli-2.1.9/sima_cli/utils/docker.py +391 -0
  14. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/vulcan/commands.py +1 -1
  15. {sima_cli-2.1.8 → sima_cli-2.1.9/sima_cli.egg-info}/PKG-INFO +3 -3
  16. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli.egg-info/SOURCES.txt +2 -0
  17. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli.egg-info/requires.txt +1 -1
  18. sima_cli-2.1.9/tests/unit/test_auth.py +251 -0
  19. sima_cli-2.1.9/tests/unit/test_auth0.py +173 -0
  20. sima_cli-2.1.9/tests/unit/test_docker_utils.py +322 -0
  21. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_elxr_update.py +159 -0
  22. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_sdk_image_detection.py +69 -6
  23. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_selfupdate.py +29 -0
  24. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_vulcan.py +7 -4
  25. sima_cli-2.1.8/sima_cli/utils/docker.py +0 -228
  26. sima_cli-2.1.8/tests/unit/test_auth.py +0 -143
  27. {sima_cli-2.1.8 → sima_cli-2.1.9}/LICENSE +0 -0
  28. {sima_cli-2.1.8 → sima_cli-2.1.9}/MANIFEST.in +0 -0
  29. {sima_cli-2.1.8 → sima_cli-2.1.9}/setup.cfg +0 -0
  30. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/__init__.py +0 -0
  31. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/__main__.py +0 -0
  32. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/app_zoo/__init__.py +0 -0
  33. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/app_zoo/app.py +0 -0
  34. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/app_zoo/commands.py +0 -0
  35. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/auth/__init__.py +0 -0
  36. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/auth/login.py +0 -0
  37. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/auth/oauth.py +0 -0
  38. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/cli.py +0 -0
  39. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/data/__init__.py +0 -0
  40. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/data/resources_internal.yaml +0 -0
  41. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/data/resources_public.yaml +0 -0
  42. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/deploy_only/__init__.py +0 -0
  43. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/deploy_only/device/__init__.py +0 -0
  44. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/deploy_only/device/commands.py +0 -0
  45. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  46. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/deploy_only/mpk/commands.py +0 -0
  47. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/discover/__init__.py +0 -0
  48. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/discover/discover.py +0 -0
  49. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/discover/linuxll.py +0 -0
  50. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/download/__init__.py +0 -0
  51. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/download/downloader.py +0 -0
  52. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/__init__.py +0 -0
  53. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/github_assets.py +0 -0
  54. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/hostdriver.py +0 -0
  55. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/metadata_info.py +0 -0
  56. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/metadata_validator.py +0 -0
  57. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/optiview.py +0 -0
  58. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/package_builder.py +0 -0
  59. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/palette.py +0 -0
  60. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/install/registry.py +0 -0
  61. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/mla/__init__.py +0 -0
  62. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/mla/meminfo.py +0 -0
  63. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/model_zoo/__init__.py +0 -0
  64. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/model_zoo/model.py +0 -0
  65. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/network/__init__.py +0 -0
  66. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/network/network.py +0 -0
  67. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/playbooks/__init__.py +0 -0
  68. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/playbooks/commands.py +0 -0
  69. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/playbooks/manager.py +0 -0
  70. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/__init__.py +0 -0
  71. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/cmdexec.py +0 -0
  72. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/commands.py +0 -0
  73. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/config.py +0 -0
  74. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/install.py +0 -0
  75. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/linux_shared_network.py +0 -0
  76. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/neat.py +0 -0
  77. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/preinstall.py +0 -0
  78. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/requirements.json +0 -0
  79. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/script.py +0 -0
  80. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/stop.py +0 -0
  81. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/sdk/uninstall.py +0 -0
  82. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/serial/__init__.py +0 -0
  83. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/serial/serial.py +0 -0
  84. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/storage/__init__.py +0 -0
  85. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/storage/nvme.py +0 -0
  86. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/storage/sdcard.py +0 -0
  87. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/__init__.py +0 -0
  88. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/bmaptool.py +0 -0
  89. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/bootimg.py +0 -0
  90. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/cleanlog.py +0 -0
  91. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/local.py +0 -0
  92. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/netboot.py +0 -0
  93. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/query.py +0 -0
  94. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/remote.py +0 -0
  95. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/update/updater.py +0 -0
  96. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/upgrade/__init__.py +0 -0
  97. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/__init__.py +0 -0
  98. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/api_common.py +0 -0
  99. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/artifactory.py +0 -0
  100. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/common.py +0 -0
  101. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/config.py +0 -0
  102. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/config_loader.py +0 -0
  103. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/container_registries.py +0 -0
  104. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/device_api.py +0 -0
  105. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/disk.py +0 -0
  106. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/env.py +0 -0
  107. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/errors.py +0 -0
  108. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/mpk_api.py +0 -0
  109. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/net.py +0 -0
  110. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/network.py +0 -0
  111. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/pcie.py +0 -0
  112. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/pkg_update_check.py +0 -0
  113. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/serializers.py +0 -0
  114. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/services.py +0 -0
  115. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/utils/tag.py +0 -0
  116. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/vulcan/__init__.py +0 -0
  117. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli/vulcan/artifacts.py +0 -0
  118. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli.egg-info/dependency_links.txt +0 -0
  119. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli.egg-info/entry_points.txt +0 -0
  120. {sima_cli-2.1.8 → sima_cli-2.1.9}/sima_cli.egg-info/top_level.txt +0 -0
  121. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/__init__.py +0 -0
  122. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/e2e/__init__.py +0 -0
  123. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/__init__.py +0 -0
  124. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_app_zoo.py +0 -0
  125. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_cli.py +0 -0
  126. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_cli_stdio.py +0 -0
  127. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_download.py +0 -0
  128. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_firmware.py +0 -0
  129. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_install_stub.py +0 -0
  130. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_metadata_installer.py +0 -0
  131. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_model_zoo.py +0 -0
  132. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_netboot.py +0 -0
  133. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_package_builder.py +0 -0
  134. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_pkg_update_check.py +0 -0
  135. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_sdk_preinstall.py +0 -0
  136. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_sdk_uninstall.py +0 -0
  137. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_skills_commands.py +0 -0
  138. {sima_cli-2.1.8 → sima_cli-2.1.9}/tests/unit/test_skills_manager.py +0 -0
  139. {sima_cli-2.1.8 → sima_cli-2.1.9}/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.8
3
+ Version: 2.1.9
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
@@ -17,7 +17,7 @@ Requires-Python: >=3.8
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: click>=8.0
20
- Requires-Dist: requests>=2.33.0
20
+ Requires-Dist: requests<3.0,>=2.32.4
21
21
  Requires-Dist: tqdm>=4.64
22
22
  Requires-Dist: pyyaml>=6.0
23
23
  Requires-Dist: paramiko>=3.5.1
@@ -163,7 +163,7 @@ 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
- - `dev` and `staging` are available for Vulcan downloads. `production` is not yet available.
166
+ - `dev`, `staging`, and `production` are available for Vulcan downloads.
167
167
  - Usage:
168
168
  - `sima-cli vulcan --env {dev|stg|staging|prd|prod|production} download [REPO] [BRANCH_OR_TAG]`
169
169
  - `sima-cli vulcan download --env {dev|stg|staging|prd|prod|production} [REPO] [BRANCH_OR_TAG]`
@@ -129,7 +129,7 @@ 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
- - `dev` and `staging` are available for Vulcan downloads. `production` is not yet available.
132
+ - `dev`, `staging`, and `production` are available for Vulcan downloads.
133
133
  - Usage:
134
134
  - `sima-cli vulcan --env {dev|stg|staging|prd|prod|production} download [REPO] [BRANCH_OR_TAG]`
135
135
  - `sima-cli vulcan download --env {dev|stg|staging|prd|prod|production} [REPO] [BRANCH_OR_TAG]`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.8"
7
+ version = "2.1.9"
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"
@@ -23,7 +23,7 @@ classifiers = [
23
23
 
24
24
  dependencies = [
25
25
  "click>=8.0",
26
- "requests>=2.33.0",
26
+ "requests>=2.32.4,<3.0",
27
27
  "tqdm>=4.64",
28
28
  "pyyaml>=6.0",
29
29
  "paramiko>=3.5.1",
@@ -1,5 +1,5 @@
1
1
  click>=8.0
2
- requests>=2.33.0
2
+ requests>=2.32.4,<3.0
3
3
  tqdm>=4.64
4
4
  pyyaml>=6.0
5
5
  paramiko>=3.5.1
@@ -18,7 +18,7 @@ setup(
18
18
  },
19
19
  install_requires=[
20
20
  "click>=8.0,<9.0",
21
- "requests>=2.33.0,<3.0",
21
+ "requests>=2.32.4,<3.0",
22
22
  "tqdm>=4.64,<5.0",
23
23
  "InquirerPy>=0.3.4,<0.4",
24
24
  "tftpy>=0.8.6,<0.9",
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.8"
2
+ __version__ = "2.1.9"
@@ -15,7 +15,7 @@ import base64
15
15
  import requests
16
16
  import webbrowser
17
17
  import click
18
- from typing import Dict, Optional
18
+ from typing import Dict, Iterable, Optional, Tuple
19
19
 
20
20
  from sima_cli.utils.config_loader import load_resource_config
21
21
 
@@ -24,6 +24,16 @@ from sima_cli.utils.config_loader import load_resource_config
24
24
  # ─────────────────────────────────────────────
25
25
  HOME_DIR = os.path.expanduser("~/.sima-cli")
26
26
  TOKEN_FILE = os.path.join(HOME_DIR, ".tokens.json")
27
+ COOKIE_FILE = os.path.join(HOME_DIR, ".sima-cli-cookies.txt")
28
+ CSRF_FILE = os.path.join(HOME_DIR, ".sima-cli-csrf.json")
29
+ PROD_USERINFO_AUDIENCE = "https://sima-ai.us.auth0.com/userinfo"
30
+ STAGING_USERINFO_AUDIENCE = "https://dev-d3sxf54xfkcifph2.us.auth0.com/userinfo"
31
+ USERINFO_AUDIENCE = PROD_USERINFO_AUDIENCE
32
+ LATEST_EULA_GRANT = "LatestEULA"
33
+ DOC_ACCESS_GRANT = "DocsAccess"
34
+ DOC_ACCESS_GRANT_ALIASES = (DOC_ACCESS_GRANT, "DocAccess")
35
+ PROD_DISCOURSE_URL = "https://developer.sima.ai/login"
36
+ STAGING_DISCOURSE_URL = "https://discourse-dev.sima.ai/login"
27
37
 
28
38
  # ─────────────────────────────────────────────
29
39
  # Configuration loader
@@ -81,6 +91,16 @@ def load_tokens() -> Optional[Dict]:
81
91
  return None
82
92
 
83
93
 
94
+ def clear_external_login_state():
95
+ """Remove cached external-auth state so the next login starts cleanly."""
96
+ for path in (TOKEN_FILE, COOKIE_FILE, CSRF_FILE):
97
+ if os.path.exists(path):
98
+ try:
99
+ os.remove(path)
100
+ except Exception as e:
101
+ print(f"⚠️ Failed to delete {path}: {e}")
102
+
103
+
84
104
  def is_token_valid(tokens: dict) -> bool:
85
105
  """Check if access token still valid based on expires_in field."""
86
106
  if not tokens:
@@ -131,6 +151,123 @@ def is_browser_available():
131
151
  except webbrowser.Error:
132
152
  return False
133
153
 
154
+
155
+ def _discourse_sign_in_url() -> str:
156
+ if os.getenv("USE_STAGING_DEV_PORTAL", "false").lower() in ("1", "true", "yes"):
157
+ return STAGING_DISCOURSE_URL
158
+ return PROD_DISCOURSE_URL
159
+
160
+
161
+ def _expected_userinfo_audience() -> str:
162
+ if os.getenv("USE_STAGING_DEV_PORTAL", "false").lower() in ("1", "true", "yes"):
163
+ return STAGING_USERINFO_AUDIENCE
164
+ return PROD_USERINFO_AUDIENCE
165
+
166
+
167
+ def _as_iterable(value) -> Iterable:
168
+ if value is None:
169
+ return []
170
+ if isinstance(value, (list, tuple, set)):
171
+ return value
172
+ return [value]
173
+
174
+
175
+ def _access_token_claims(tokens: dict) -> dict:
176
+ return decode_jwt_payload(tokens.get("access_token", ""))
177
+
178
+
179
+ def _access_token_has_userinfo_audience(claims: dict) -> bool:
180
+ return _expected_userinfo_audience() in _as_iterable(claims.get("aud"))
181
+
182
+
183
+ def _access_token_has_grant(claims: dict, grant: str) -> bool:
184
+ grant_names = DOC_ACCESS_GRANT_ALIASES if grant in DOC_ACCESS_GRANT_ALIASES else (grant,)
185
+ for claim_name, claim_value in claims.items():
186
+ if claim_name in ("permissions", "grants", "roles") or claim_name.endswith(("/permissions", "/grants", "/roles")):
187
+ if any(grant_name in _as_iterable(claim_value) for grant_name in grant_names):
188
+ return True
189
+
190
+ scope = claims.get("scope")
191
+ if isinstance(scope, str) and any(grant_name in scope.split() for grant_name in grant_names):
192
+ return True
193
+
194
+ return False
195
+
196
+
197
+ def access_token_has_doc_access(tokens: dict) -> bool:
198
+ return _access_token_has_grant(_access_token_claims(tokens), DOC_ACCESS_GRANT)
199
+
200
+
201
+ def access_token_has_latest_eula(tokens: dict) -> bool:
202
+ return _access_token_has_grant(_access_token_claims(tokens), LATEST_EULA_GRANT)
203
+
204
+
205
+ def _validate_access_token_requirements(tokens: dict) -> Tuple[bool, Dict[str, bool]]:
206
+ claims = _access_token_claims(tokens)
207
+ checks = {
208
+ "doc_access": _access_token_has_grant(claims, DOC_ACCESS_GRANT),
209
+ "latest_eula": _access_token_has_grant(claims, LATEST_EULA_GRANT),
210
+ "userinfo_audience": _access_token_has_userinfo_audience(claims),
211
+ }
212
+ return checks["doc_access"] and checks["latest_eula"] and checks["userinfo_audience"], checks
213
+
214
+
215
+ def _prompt_for_discourse_sign_in(checks: Dict[str, bool]) -> bool:
216
+ discourse_url = _discourse_sign_in_url()
217
+ missing = []
218
+ if not checks.get("latest_eula"):
219
+ missing.append("LatestEULA grant")
220
+ if not checks.get("userinfo_audience"):
221
+ missing.append(f"{_expected_userinfo_audience()} audience")
222
+
223
+ click.echo("")
224
+ click.secho("Developer Portal sign-in is required before sima-cli can continue.", fg="yellow")
225
+ if missing:
226
+ click.echo(f"Missing from the access token: {', '.join(missing)}.")
227
+ click.echo("Please sign in to Discourse / Developer Portal and accept the EULA if prompted.")
228
+
229
+ if is_browser_available():
230
+ click.echo(f"Opening sign-in page: {click.style(discourse_url, fg='cyan', bold=True)}")
231
+ try:
232
+ webbrowser.open(discourse_url)
233
+ except Exception:
234
+ click.echo("Browser could not be opened automatically. Open this link manually:")
235
+ click.secho(discourse_url, fg="cyan", bold=True)
236
+ else:
237
+ click.echo("Browser not available. Open this link manually:")
238
+ click.secho(discourse_url, fg="cyan", bold=True)
239
+
240
+ return click.confirm("Have you signed in to Discourse / Developer Portal?", default=True)
241
+
242
+
243
+ def _ensure_access_token_requirements(tokens: dict, auth_cfg: dict) -> Optional[dict]:
244
+ while True:
245
+ valid, checks = _validate_access_token_requirements(tokens)
246
+ if valid:
247
+ return tokens
248
+ if not checks.get("doc_access"):
249
+ return tokens
250
+
251
+ if not _prompt_for_discourse_sign_in(checks):
252
+ clear_external_login_state()
253
+ click.echo("Logged out. Run `sima-cli login` again after signing in to Developer Portal.")
254
+ return None
255
+
256
+ refresh_token = tokens.get("refresh_token")
257
+ if not refresh_token:
258
+ click.echo("Unable to refresh your access token after Developer Portal sign-in.")
259
+ clear_external_login_state()
260
+ click.echo("Logged out. Run `sima-cli login` again.")
261
+ return None
262
+
263
+ refreshed = refresh_access_token(auth_cfg, refresh_token)
264
+ if not refreshed:
265
+ clear_external_login_state()
266
+ click.echo("Logged out. Run `sima-cli login` again.")
267
+ return None
268
+
269
+ tokens = refreshed
270
+
134
271
  def request_device_code(auth_cfg):
135
272
  """Step 1: Request device code from Auth0."""
136
273
  data = {
@@ -269,16 +406,25 @@ def get_or_refresh_tokens(force=False):
269
406
  tokens = load_tokens()
270
407
 
271
408
  if tokens and is_token_valid(tokens) and not force:
272
- return tokens
409
+ tokens = _ensure_access_token_requirements(tokens, auth_cfg)
410
+ if tokens:
411
+ return tokens
412
+ return None
273
413
 
274
414
  if tokens and tokens.get("refresh_token"):
275
415
  refreshed = refresh_access_token(auth_cfg, tokens["refresh_token"])
276
416
  if refreshed:
277
- print_welcome_message(refreshed)
278
- return refreshed
417
+ refreshed = _ensure_access_token_requirements(refreshed, auth_cfg)
418
+ if refreshed:
419
+ print_welcome_message(refreshed)
420
+ return refreshed
421
+ return None
279
422
  print("⚠️ Refresh failed, falling back to new login.")
280
423
 
281
424
  new_tokens = login_auth0(auth_cfg)
425
+ new_tokens = _ensure_access_token_requirements(new_tokens, auth_cfg)
426
+ if not new_tokens:
427
+ return None
282
428
  print_welcome_message(new_tokens)
283
429
  return new_tokens
284
430
 
@@ -7,6 +7,7 @@ import subprocess
7
7
  import shutil
8
8
  import base64
9
9
  import hashlib
10
+ import webbrowser
10
11
 
11
12
  from typing import Optional
12
13
  from http.cookiejar import MozillaCookieJar
@@ -17,9 +18,14 @@ from sima_cli.utils.env import is_sima_board
17
18
  from sima_cli.auth.auth0 import (
18
19
  decode_jwt_payload,
19
20
  extract_email,
21
+ access_token_has_doc_access,
22
+ access_token_has_latest_eula,
23
+ get_auth_config,
20
24
  get_or_refresh_tokens,
21
25
  get_cached_access_token,
26
+ is_browser_available,
22
27
  load_tokens,
28
+ refresh_access_token,
23
29
  )
24
30
 
25
31
  HOME_DIR = os.path.expanduser("~/.sima-cli")
@@ -43,6 +49,7 @@ else:
43
49
 
44
50
  # Derived endpoints
45
51
  LOGIN_URL = f"{DEV_PORTAL}/session"
52
+ DEV_PORTAL_LOGIN_URL = f"{DEV_PORTAL}/login"
46
53
  DUMMY_CHECK_URL = f"{DOCS_PORTAL}/pkg_downloads/validation"
47
54
  ACCESS_REQUEST_FORM_URL = "https://www2.sima.ai/l/1041271/2025-05-05/37bndg"
48
55
  USER_INFO_CLAIM = "https://auth.sima.ai/user_info"
@@ -61,14 +68,39 @@ HEADERS = {
61
68
  "sec-ch-ua-platform": '"macOS"',
62
69
  }
63
70
 
71
+
72
+ def _prompt_manual_developer_portal_login(confirm_completion: bool = False) -> bool:
73
+ click.secho(
74
+ f"\nOpen this page to accept EULA to proceed, press Y when you are done:\n{DEV_PORTAL_LOGIN_URL}\n",
75
+ fg="green",
76
+ )
77
+ if confirm_completion:
78
+ return click.confirm("Have you accepted the EULA?", default=True)
79
+ return False
80
+
81
+
82
+ def _open_developer_portal_login_page(confirm_manual_completion: bool = False) -> bool:
83
+ if not is_browser_available():
84
+ return _prompt_manual_developer_portal_login(confirm_manual_completion)
85
+
86
+ try:
87
+ opened = webbrowser.open(DEV_PORTAL_LOGIN_URL)
88
+ except Exception:
89
+ opened = False
90
+
91
+ if not opened:
92
+ return _prompt_manual_developer_portal_login(confirm_manual_completion)
93
+ return True
94
+
95
+
64
96
  def _handle_eula_flow(session: requests.Session, username: str, domain: str) -> bool:
65
97
  try:
66
98
  click.echo("\n📄 To continue, you must accept the End-User License Agreement (EULA).")
67
- click.echo("👉 Please sign in to Developer Portal on your browser, then open the following URL to accept the EULA:")
68
- click.echo("👉 If you were not prompted with the EULA acceptance popup, please open the URL in the incogniton browser.")
69
- click.echo(f"\n {DUMMY_CHECK_URL}\n")
99
+ click.echo("👉 Please sign in to Developer Portal in your browser and accept the EULA if prompted.")
100
+ click.echo("👉 If you were not prompted with the EULA acceptance popup, try opening the page in an incognito browser.")
101
+ _open_developer_portal_login_page()
70
102
 
71
- if not click.confirm("✅ Have you completed the EULA form in your browser?", default=True):
103
+ if not click.confirm("✅ Have you signed in to Developer Portal and accepted the EULA?", default=True):
72
104
  click.echo("❌ EULA acceptance is required to continue.")
73
105
  return False
74
106
 
@@ -192,15 +224,20 @@ def _show_access_request_pending_message(already_submitted: bool = False):
192
224
  click.secho("✅ Your access request has already been submitted.", fg="green")
193
225
  else:
194
226
  click.secho("✅ Your access request has been submitted.", fg="green")
227
+
228
+
229
+ def _show_limited_access_pending_message():
230
+ click.secho("✅ You are signed in with limited Developer Portal access.", fg="green")
195
231
  console.print(
196
232
  Panel(
197
233
  "\n".join(
198
234
  [
199
- "SiMa is reviewing your request and will grant access shortly.",
235
+ "SiMa is reviewing your account and will grant full access shortly.",
200
236
  "Please look out for an email from marketing@marketing.sima.ai.",
201
- "Once access is granted, run `sima-cli login` again.",
202
237
  ]
203
238
  ),
239
+ title="Notice",
240
+ title_align="center",
204
241
  border_style="yellow",
205
242
  style="yellow",
206
243
  expand=False,
@@ -208,27 +245,26 @@ def _show_access_request_pending_message(already_submitted: bool = False):
208
245
  )
209
246
 
210
247
 
211
- def _show_access_request_info_panel():
212
- console.print(
213
- Panel(
214
- "\n".join(
215
- [
216
- "Welcome to the SiMa.ai Developer Portal.",
217
- "",
218
- "To download digital assets from the Developer Portal, "
219
- "SiMa's business team needs to grant access after you "
220
- "provide a few additional details, including your project goal.",
221
- "",
222
- "Once approved, you will receive an email from "
223
- "marketing@marketing.sima.ai. Please check your email "
224
- "client's spam filter.",
225
- ]
226
- ),
227
- title="Developer Portal Access Request",
228
- border_style="yellow",
229
- expand=False,
230
- )
231
- )
248
+ def _prompt_eula_acceptance_after_access_request() -> bool:
249
+ click.echo("\n📄 To continue, accept the End-User License Agreement (EULA).")
250
+ click.echo("👉 Please sign in to Developer Portal in your browser and accept the EULA if prompted.")
251
+ _open_developer_portal_login_page()
252
+ if not click.confirm("✅ Have you signed in to Developer Portal and accepted the EULA?", default=True):
253
+ return False
254
+
255
+ tokens = load_tokens() or {}
256
+ refresh_token = tokens.get("refresh_token")
257
+ if not refresh_token:
258
+ click.secho("⚠️ Unable to refresh your access token: missing refresh token.", fg="yellow")
259
+ return False
260
+
261
+ click.echo("Refreshing access token after EULA acceptance...")
262
+ refreshed = refresh_access_token(get_auth_config(), refresh_token)
263
+ if not refreshed:
264
+ click.secho("⚠️ Unable to refresh your access token after EULA acceptance.", fg="yellow")
265
+ return False
266
+
267
+ return True
232
268
 
233
269
 
234
270
  def _submit_access_request() -> bool:
@@ -243,19 +279,10 @@ def _submit_access_request() -> bool:
243
279
  )
244
280
  return False
245
281
 
246
- if _has_submitted_access_request(claims):
247
- _show_access_request_pending_message(already_submitted=True)
248
- _logout_external_credentials()
249
- return False
250
-
251
- _show_access_request_info_panel()
252
- message = click.prompt(
253
- click.style("Please briefly describe your project", fg="yellow"),
254
- type=str,
255
- ).strip()
256
- payload = _build_access_request_payload(claims, message)
282
+ payload = _build_access_request_payload(claims, "sima-cli sign up request")
257
283
 
258
284
  try:
285
+ click.echo("Submitting Developer Portal access request...")
259
286
  response = requests.post(ACCESS_REQUEST_FORM_URL, data=payload, timeout=15)
260
287
  response.raise_for_status()
261
288
  except requests.RequestException as e:
@@ -264,7 +291,8 @@ def _submit_access_request() -> bool:
264
291
 
265
292
  _mark_access_request_submitted(claims)
266
293
  _show_access_request_pending_message()
267
- _logout_external_credentials()
294
+ if not access_token_has_latest_eula(load_tokens() or {}):
295
+ _prompt_eula_acceptance_after_access_request()
268
296
  return False
269
297
 
270
298
 
@@ -355,7 +383,12 @@ def login_external(force=False, loginDocker=True):
355
383
  if _ACCESS_REQUEST_HANDLED:
356
384
  return None
357
385
 
358
- get_or_refresh_tokens(force=force)
386
+ tokens = get_or_refresh_tokens(force=force)
387
+ if not tokens:
388
+ return None
389
+ if not access_token_has_doc_access(tokens):
390
+ return _submit_access_request()
391
+
359
392
  session, valid = validate_session()
360
393
  if valid:
361
394
  if loginDocker:
@@ -12,7 +12,7 @@ import shlex
12
12
  import platform
13
13
  import hashlib
14
14
  from urllib.parse import urlparse, quote, urljoin, unquote
15
- from typing import Dict
15
+ from typing import Dict, List
16
16
  from tqdm import tqdm
17
17
  from pathlib import Path
18
18
  import subprocess
@@ -298,7 +298,7 @@ def _resolve_resource_url(base_url: str, resource: str) -> str:
298
298
  )
299
299
  return urljoin(base_url, encoded_resource)
300
300
 
301
- def _resolve_resource_url_candidates(base_url: str, resource: str) -> list[str]:
301
+ def _resolve_resource_url_candidates(base_url: str, resource: str) -> List[str]:
302
302
  primary_url = _resolve_resource_url(base_url, resource)
303
303
 
304
304
  parsed_resource = urlparse(resource)
@@ -338,12 +338,12 @@ def _normalize_downloaded_metadata_resource(local_path: str, expected_path: Path
338
338
 
339
339
  def _download_metadata_file_resource(
340
340
  resource: str,
341
- resource_urls: list[str],
341
+ resource_urls: List[str],
342
342
  dest_folder: str,
343
343
  dest_path: Path,
344
344
  internal: bool,
345
345
  ) -> str:
346
- errors: list[str] = []
346
+ errors: List[str] = []
347
347
  for index, resource_url in enumerate(resource_urls):
348
348
  try:
349
349
  local_path = download_file_from_url(
@@ -347,19 +347,22 @@ def prompt_multi_select(baseline_present: bool = False) -> List[str]:
347
347
 
348
348
  return deduped
349
349
 
350
- def run_command(cmd, capture_output=False):
350
+ def run_command(cmd, capture_output=False, fatal=True):
351
351
  """Run a shell command and return output if requested."""
352
352
  try:
353
353
  if capture_output:
354
354
  return subprocess.check_output(cmd, text=True).strip()
355
355
  else:
356
356
  subprocess.run(cmd, check=True)
357
- return None
357
+ return True if not fatal else None
358
358
  except subprocess.CalledProcessError as e:
359
- print(f"❌ Command failed: {' '.join(cmd)}")
359
+ prefix = "❌ Command failed" if fatal else "⚠️ Optional command failed"
360
+ print(f"{prefix}: {' '.join(cmd)}")
360
361
  if e.stderr:
361
362
  print(f"Error: {e.stderr.strip()}")
362
- sys.exit(1)
363
+ if fatal:
364
+ sys.exit(1)
365
+ return False
363
366
 
364
367
 
365
368
  ### Dynamic port allocation
@@ -664,7 +667,7 @@ def _copy_sima_cli_auth_cache_to_container(sdk_container_name: str, login_name:
664
667
  return
665
668
 
666
669
  container_auth_dir = f"/home/{login_name}/.sima-cli"
667
- run_command([
670
+ if not run_command([
668
671
  "docker",
669
672
  "exec",
670
673
  "-u",
@@ -673,14 +676,29 @@ def _copy_sima_cli_auth_cache_to_container(sdk_container_name: str, login_name:
673
676
  "mkdir",
674
677
  "-p",
675
678
  container_auth_dir,
676
- ])
679
+ ], fatal=False):
680
+ print("⚠️ Could not create sima-cli auth cache directory in Neat SDK container; continuing setup.")
681
+ return
682
+ if not run_command([
683
+ "docker",
684
+ "exec",
685
+ "-u",
686
+ "root",
687
+ sdk_container_name,
688
+ "chown",
689
+ f"{uid}:{gid}",
690
+ container_auth_dir,
691
+ ], fatal=False):
692
+ print("⚠️ Could not update ownership for sima-cli auth cache directory; continuing setup.")
677
693
 
678
694
  copied = []
679
695
  for filename in existing_files:
680
696
  host_path = os.path.join(host_sima_cli_dir, filename)
681
697
  container_path = f"{container_auth_dir}/{filename}"
682
- run_command(["docker", "cp", host_path, f"{sdk_container_name}:{container_path}"])
683
- run_command([
698
+ if not run_command(["docker", "cp", host_path, f"{sdk_container_name}:{container_path}"], fatal=False):
699
+ print(f"⚠️ Could not copy host sima-cli auth cache file '{filename}' into Neat SDK container; continuing setup.")
700
+ continue
701
+ if not run_command([
684
702
  "docker",
685
703
  "exec",
686
704
  "-u",
@@ -689,19 +707,15 @@ def _copy_sima_cli_auth_cache_to_container(sdk_container_name: str, login_name:
689
707
  "chown",
690
708
  f"{uid}:{gid}",
691
709
  container_path,
692
- ])
710
+ ], fatal=False):
711
+ print(f"⚠️ Could not update ownership for copied sima-cli auth cache file '{filename}'; continuing setup.")
712
+ continue
693
713
  copied.append(filename)
694
714
 
695
- run_command([
696
- "docker",
697
- "exec",
698
- "-u",
699
- "root",
700
- sdk_container_name,
701
- "chown",
702
- f"{uid}:{gid}",
703
- container_auth_dir,
704
- ])
715
+ if not copied:
716
+ print("ℹ️ No sima-cli auth cache files were copied into Neat SDK container; continuing setup.")
717
+ return
718
+
705
719
  print(f"🔐 Copied sima-cli auth cache file(s) into Neat SDK container: {', '.join(copied)}")
706
720
 
707
721
 
@@ -918,6 +932,8 @@ def install_neat_playbooks(sdk_container_name: str, login_name: str) -> None:
918
932
  login_name,
919
933
  "-e",
920
934
  "SIMA_CLI_CHECK_FOR_UPDATE=0",
935
+ "-e",
936
+ "GITHUB_TOKEN",
921
937
  sdk_container_name,
922
938
  "bash",
923
939
  "-lc",