sima-cli 0.0.39__py3-none-any.whl → 0.0.40__py3-none-any.whl

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.
sima_cli/__version__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "0.0.39"
2
+ __version__ = "0.0.40"
@@ -3,24 +3,40 @@ import click
3
3
  import getpass
4
4
  import requests
5
5
  import json
6
- import tempfile
6
+ import subprocess
7
+ import shutil
8
+ import base64
7
9
 
10
+ from typing import Optional
8
11
  from http.cookiejar import MozillaCookieJar
9
12
  from sima_cli.__version__ import __version__
13
+ from sima_cli.utils.env import is_sima_board
10
14
 
11
15
  HOME_DIR = os.path.expanduser("~/.sima-cli")
12
16
  COOKIE_JAR_PATH = os.path.join(HOME_DIR, ".sima-cli-cookies.txt")
13
17
  CSRF_PATH = os.path.join(HOME_DIR, ".sima-cli-csrf.json")
14
18
 
15
- CSRF_URL = "https://developer.sima.ai/session/csrf"
16
- LOGIN_URL = "https://developer.sima.ai/session"
17
- DUMMY_CHECK_URL = "https://docs.sima.ai/pkg_downloads/validation"
19
+ # Base URLs depending on environment
20
+ # Detect staging or production environment
21
+ if os.getenv("USE_STAGING_DEV_PORTAL", "false").lower() in ("1", "true", "yes"):
22
+ DEV_PORTAL = "https://discourse-dev.sima.ai"
23
+ DOCS_PORTAL = "https://docs-dev.sima.ai"
24
+ click.secho("⚠️ Using STAGING developer portal: discourse-dev.sima.ai", fg="red", bold=True)
25
+ else:
26
+ DEV_PORTAL = "https://developer.sima.ai"
27
+ DOCS_PORTAL = "https://docs.sima.ai"
28
+
29
+ # Derived endpoints
30
+ CSRF_URL = f"{DEV_PORTAL}/session/csrf"
31
+ LOGIN_URL = f"{DEV_PORTAL}/session"
32
+ DUMMY_CHECK_URL = f"{DOCS_PORTAL}/pkg_downloads/validation"
33
+
18
34
 
19
35
  HEADERS = {
20
36
  "User-Agent": f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) sima-cli/{__version__} Chrome/137.0.0.0 Safari/537.36",
21
37
  "X-Requested-With": "XMLHttpRequest",
22
- "Referer": "https://developer.sima.ai/login",
23
- "Origin": "https://developer.sima.ai",
38
+ "Referer": f"{DEV_PORTAL}/login",
39
+ "Origin": f"{DEV_PORTAL}",
24
40
  "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
25
41
  "sec-ch-ua": '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
26
42
  "sec-ch-ua-mobile": "?0",
@@ -62,6 +78,35 @@ def _is_session_valid(session: requests.Session) -> bool:
62
78
  click.echo(f"❌ Error validating session: {e}")
63
79
  return False
64
80
 
81
+ def get_ecr_access_info(session: requests.Session) -> Optional[dict]:
82
+ """
83
+ Retrieve an ECR access token and proxy endpoint using the current authenticated session.
84
+
85
+ Args:
86
+ session (requests.Session): An authenticated session (with valid cookies).
87
+
88
+ Returns:
89
+ Optional[dict]: JSON response containing the ECR token info, or None if failed.
90
+ """
91
+ try:
92
+ ecr_url = f"{DOCS_PORTAL}/pkg_downloads/ecr-token"
93
+ response = session.get(ecr_url, timeout=10)
94
+ response.raise_for_status()
95
+
96
+ data = response.json()
97
+ if "authorizationToken" in data and "proxyEndpoint" in data:
98
+ return data
99
+ else:
100
+ click.secho(
101
+ "⚠️ Container registry token response missing 'authorizationToken'. "
102
+ "Contact support@sima.ai for help.",
103
+ fg="yellow",
104
+ )
105
+ return data
106
+ except Exception as e:
107
+ click.secho(f"❌ Failed to retrieve ECR token: {e}", fg="red")
108
+ return None
109
+
65
110
  def _delete_auth_files():
66
111
  for path in [COOKIE_JAR_PATH, CSRF_PATH]:
67
112
  if os.path.exists(path):
@@ -106,7 +151,6 @@ def _fetch_and_store_csrf_token(session: requests.Session) -> str:
106
151
  click.echo(f"❌ Failed to fetch CSRF token: {e}")
107
152
  return ""
108
153
 
109
-
110
154
  def login_external():
111
155
  """Interactive login workflow with CSRF token, cookie caching, and TOTP handling."""
112
156
  for attempt in range(1, 4):
@@ -163,7 +207,9 @@ def login_external():
163
207
  if status == 200 and _success():
164
208
  _save_cookie_jar(session)
165
209
  welcome = (j.get("users", [{}])[0].get("name") if isinstance(j, dict) else "") or ""
166
- click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
210
+ click.secho(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!", fg = 'green')
211
+ token, endpoint = resolve_public_registry()
212
+ docker_login_with_token('sima_cli', token, endpoint)
167
213
  return session
168
214
 
169
215
  # See if TOTP is required/invalid; then prompt and retry up to 3 times
@@ -186,7 +232,9 @@ def login_external():
186
232
  if status == 200 and (j2 and j2.get("ok") is True or _is_session_valid(session)):
187
233
  _save_cookie_jar(session)
188
234
  welcome = (j2.get("users", [{}])[0].get("name") if isinstance(j2, dict) else "") or ""
189
- click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
235
+ click.secho(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!", fg = 'green')
236
+ token, endpoint = resolve_public_registry()
237
+ docker_login_with_token('sima_cli', token, endpoint)
190
238
  return session
191
239
 
192
240
  # If still invalid 2FA, let user try again; otherwise break to outer loop
@@ -215,9 +263,107 @@ def login_external():
215
263
  else:
216
264
  err_detail = str(raw)[:200]
217
265
 
218
- click.echo(f"❌ Server response code: {raw}")
266
+ click.echo(f"❌ Server response code: {json.dumps(raw)}")
219
267
  click.echo(f"❌ Login failed. {err_detail or 'Please check your credentials and try again.'}")
220
268
 
221
269
  click.echo("❌ Login failed after 3 attempts.")
222
270
  raise SystemExit(1)
223
271
 
272
+ def resolve_public_registry(name: str = 'ecr'):
273
+ """
274
+ Resolve a short registry alias to its corresponding public container
275
+ registry endpoint and ensure authentication if required.
276
+
277
+ Args:
278
+ name (str): Short alias for a known registry (e.g. 'ecr').
279
+
280
+ Returns:
281
+ Optional[str]: The resolved registry endpoint, or None on failure.
282
+ """
283
+ name = name.lower().strip()
284
+
285
+ if not ensure_docker_available():
286
+ return None, None
287
+
288
+ if name == "ecr":
289
+ click.secho("🔍 Resolving SiMa.ai container registry...", fg="cyan")
290
+
291
+ try:
292
+ session = login_external()
293
+ if not session or not isinstance(session, requests.Session):
294
+ click.secho("❌ No valid session found. Please login first using `sima-cli login`.", fg="red")
295
+ return None, None
296
+
297
+ ecr_info = get_ecr_access_info(session)
298
+ if not ecr_info:
299
+ click.secho("❌ Failed to retrieve container registry token information.", fg="red")
300
+ return None, None
301
+
302
+ token = ecr_info.get("authorizationToken")
303
+ endpoint = ecr_info.get("proxyEndpoint")
304
+ if not token or not endpoint:
305
+ click.secho("⚠️ Missing 'authorizationToken' or 'proxyEndpoint' in container registry response.", fg="yellow")
306
+ return None, None
307
+
308
+ return token, endpoint
309
+
310
+ except Exception as e:
311
+ click.secho(f"❌ Unexpected error while resolving registry '{name}': {e}", fg="red")
312
+ return None, None
313
+
314
+ else:
315
+ raise click.ClickException(f"❌ Unknown public registry alias: {name}")
316
+
317
+
318
+ def ensure_docker_available() -> bool:
319
+ """
320
+ Check if Docker CLI exists on PATH.
321
+
322
+ - Returns True if Docker is available.
323
+ - If missing, prints a gentle warning (only on non-SiMa hosts).
324
+ - On SiMa boards (Modalix/Davinci), stays completely silent since Docker is optional.
325
+ """
326
+ if shutil.which("docker"):
327
+ return True
328
+
329
+ # Only warn on host systems, not on SiMa devkits
330
+ if not is_sima_board():
331
+ click.echo("⚠️ Docker CLI not found — container image pull and registry operations will be skipped until Docker is installed.")
332
+
333
+ return False
334
+
335
+ def docker_login_with_token(username: str, token: str, registry: str = "artifacts.eng.sima.ai"):
336
+ """
337
+ Use `docker login` with --password-stdin to register Artifactory or ECR token.
338
+
339
+ - Automatically detects and decodes AWS ECR base64 tokens of the form 'QVdTOmV5...'
340
+ - Ensures Docker is available before attempting login.
341
+ - Works even when Docker uses credential helpers (e.g., credsStore=desktop).
342
+ """
343
+ ensure_docker_available()
344
+
345
+ # Decode if token looks like base64 (AWS ECR style)
346
+ try:
347
+ decoded = base64.b64decode(token).decode("utf-8")
348
+ if decoded.startswith("AWS:"):
349
+ # ECR token detected → force username to AWS
350
+ password = decoded.split("AWS:", 1)[1]
351
+ username = "AWS"
352
+ else:
353
+ password = token
354
+ except Exception:
355
+ # Not a valid base64 string — use raw token
356
+ password = token
357
+
358
+ proc = subprocess.run(
359
+ ["docker", "login", registry, "-u", username, "--password-stdin"],
360
+ input=password.encode(),
361
+ stdout=subprocess.PIPE,
362
+ stderr=subprocess.PIPE,
363
+ )
364
+
365
+ if proc.returncode != 0:
366
+ raise click.ClickException(f"❌ Docker login failed: {proc.stderr.decode().strip()}")
367
+
368
+ click.echo(proc.stdout.decode().strip() or f"✅ Logged in to SiMa.ai container registry")
369
+ return password
sima_cli/auth/login.py CHANGED
@@ -4,7 +4,7 @@ import requests
4
4
  from sima_cli.utils.config import set_auth_token, get_auth_token
5
5
  from sima_cli.utils.config_loader import load_resource_config, artifactory_url
6
6
  from sima_cli.utils.artifactory import exchange_identity_token, validate_token
7
- from sima_cli.auth.basic_auth import login_external
7
+ from sima_cli.auth.basic_auth import login_external, docker_login_with_token
8
8
 
9
9
  def login(method: str = "external"):
10
10
  """
@@ -30,6 +30,7 @@ def login_internal():
30
30
  2. Validate the token using the configured validation URL.
31
31
  3. If valid, exchange it for a short-lived access token.
32
32
  4. Save the short-lived token to local config.
33
+ 5. Also calls docker to login
33
34
  """
34
35
 
35
36
  cfg = load_resource_config()
@@ -40,7 +41,6 @@ def login_internal():
40
41
  validate_url = f"{base_url}/{validate_url}"
41
42
  exchange_url = f"{base_url}/{internal_url}"
42
43
 
43
- # Check for required config values
44
44
  if not validate_url or not exchange_url:
45
45
  click.echo("❌ Missing 'validate_url' or 'internal_url' in internal auth config.")
46
46
  click.echo("👉 Please check ~/.sima-cli/resources_internal.yaml")
@@ -58,7 +58,7 @@ def login_internal():
58
58
  if not is_valid:
59
59
  return click.echo("❌ Token validation failed. Please check your identity token.")
60
60
 
61
- click.echo(f"✅ Identity token is valid")
61
+ click.echo(f"✅ Identity token is valid for {username}")
62
62
 
63
63
  # Step 2: Exchange for a short-lived access token (default: 30 days)
64
64
  access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=2592000)
@@ -70,41 +70,7 @@ def login_internal():
70
70
  set_auth_token(access_token, internal=True)
71
71
  click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 30 days).")
72
72
 
73
-
74
- def _login_external():
75
- """
76
- External login using Developer Portal endpoint defined in the 'public' section of YAML config.
77
- Prompts for username/password and retrieves access token.
78
- """
79
-
80
- cfg = load_resource_config()
81
- auth_url = cfg.get("public", {}).get("auth", {}).get("auth_url")
82
-
83
- if not auth_url:
84
- click.echo("❌ External auth URL not configured in YAML.")
85
- return
86
-
87
- click.echo("🌐 Logging in using external Developer Portal...")
88
-
89
- # Prompt for credentials
90
- username = click.prompt("Email or Username")
91
- password = getpass.getpass("Password: ")
92
-
93
- data = {
94
- "username": username,
95
- "password": password
96
- }
97
-
98
73
  try:
99
- response = requests.post(auth_url, json=data)
100
- response.raise_for_status()
101
-
102
- token = response.json().get("access_token")
103
- if not token:
104
- return click.echo("❌ Failed to retrieve access token.")
105
-
106
- set_auth_token(token)
107
- click.echo("✅ External login successful.")
108
-
109
- except requests.RequestException as e:
110
- click.echo(f"❌ External login failed: {e}")
74
+ docker_login_with_token(user_name, access_token)
75
+ except Exception as e:
76
+ click.echo(f"⚠️ Docker credential setup failed: {e}")
sima_cli/cli.py CHANGED
@@ -16,6 +16,8 @@ from sima_cli.storage.nvme import nvme_format, nvme_remount
16
16
  from sima_cli.storage.sdcard import sdcard_format
17
17
  from sima_cli.network.network import network_menu
18
18
  from sima_cli.utils.pkg_update_check import check_for_update
19
+ from sima_cli.utils.container_registries import docker_logout_from_registry, install_from_cr
20
+ from sima_cli.utils.env import is_devkit_running_elxr
19
21
 
20
22
  # Entry point for the CLI tool using Click's command group decorator
21
23
  @click.group()
@@ -89,6 +91,7 @@ def logout_cmd(ctx):
89
91
 
90
92
  if internal:
91
93
  target_files = ["config.json"]
94
+ docker_logout_from_registry()
92
95
  else:
93
96
  target_files = [".sima-cli-cookies.txt", ".sima-cli-csrf.json"]
94
97
 
@@ -140,7 +143,7 @@ def download(ctx, url, dest):
140
143
  # Update Command
141
144
  # ----------------------
142
145
  @main.command(name="update")
143
- @click.argument('version_or_url')
146
+ @click.argument('version_or_url', required=False)
144
147
  @click.option('--ip', help="Target device IP address for remote firmware update.")
145
148
  @click.option(
146
149
  '-y', '--yes',
@@ -159,8 +162,14 @@ def update(ctx, version_or_url, ip, yes, passwd, flavor):
159
162
  Run system update across different environments.
160
163
  Downloads and applies firmware updates for PCIe host or SiMa board.
161
164
 
162
- version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package.
165
+ version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package or a local bundle file.
163
166
  """
167
+ if version_or_url is None:
168
+ if not is_devkit_running_elxr():
169
+ raise click.UsageError("version_or_url is required on non-ELXR DevKit systems")
170
+ else:
171
+ click.echo(f"➡️ Updating with {version_or_url}")
172
+
164
173
  internal = ctx.obj.get("internal", False)
165
174
  perform_update(version_or_url, ip, internal, passwd=passwd, auto_confirm=yes, flavor=flavor)
166
175
 
@@ -386,17 +395,21 @@ def install_cmd(ctx, component, version, mirror, tag):
386
395
  click.echo("❌ You must specify either a component name or provide --metadata.")
387
396
  ctx.exit(1)
388
397
 
389
- component = component.lower()
390
-
391
398
  # if user specified gh: as component, treat it the same as -m
392
399
  if component.startswith("gh:"):
393
400
  return install_from_metadata(metadata_url=component, internal=False)
401
+
402
+ # if the user specified cr: as component, install from container registry
403
+ if component.startswith("cr:"):
404
+ return install_from_cr(resource_spec=component, internal=internal)
394
405
 
395
406
  # Validate version requirement
396
407
  if component in SDK_DEPENDENT_COMPONENTS and not version:
397
408
  click.echo(f"❌ The component '{component}' requires a specific SDK version. Please provide one using -v.")
398
409
  ctx.exit(1)
399
410
 
411
+ component = component.lower()
412
+
400
413
  if component in SDK_INDEPENDENT_COMPONENTS and version:
401
414
  click.echo(f"ℹ️ The component '{component}' does not require an SDK version. Ignoring -v {version}.")
402
415
 
@@ -26,6 +26,7 @@ from sima_cli.utils.env import get_environment_type, get_exact_devkit_type, get_
26
26
  from sima_cli.download.downloader import download_file_from_url
27
27
  from sima_cli.install.metadata_validator import validate_metadata, MetadataValidationError
28
28
  from sima_cli.install.metadata_info import print_metadata_summary, parse_size_string_to_bytes
29
+ from sima_cli.utils.container_registries import install_from_cr
29
30
 
30
31
  console = Console()
31
32
 
@@ -260,16 +261,22 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
260
261
  """
261
262
  Downloads resources defined in metadata to a local destination folder.
262
263
 
264
+ Supports resource types:
265
+ - Regular files or URLs
266
+ - Hugging Face repos (hf:<repo_id>@revision)
267
+ - GitHub repos (gh:<owner>/<repo>@ref)
268
+ - Container registries (cr:<registry>/<image>[:tag])
269
+
263
270
  Args:
264
271
  metadata (dict): Parsed and validated metadata
265
272
  base_url (str): Base URL of the metadata file (used to resolve relative resource paths)
266
273
  dest_folder (str): Local path to download resources into
267
- internal (bool): Whether to use internal download routing (if applicable)
274
+ internal (bool): Whether to use internal routing (e.g., Artifactory Docker registry)
268
275
  skip_models (bool): If True, skips downloading any file path starting with 'models/'
269
- tag (str): metadata.json tag from GitHub will be passed into the resources in the file
276
+ tag (str): metadata.json tag from GitHub passed into resources if applicable
270
277
 
271
278
  Returns:
272
- list: Paths to the downloaded local files
279
+ list: Paths to the downloaded local files or pulled container image identifiers
273
280
  """
274
281
  resources = metadata.get("resources", [])
275
282
  if not resources:
@@ -278,6 +285,7 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
278
285
  os.makedirs(dest_folder, exist_ok=True)
279
286
  local_paths = []
280
287
 
288
+ # Filter model files if needed
281
289
  filtered_resources = []
282
290
  for r in resources:
283
291
  if skip_models and r.strip().lower().startswith("models/"):
@@ -318,9 +326,9 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
318
326
  local_paths.append(model_path)
319
327
  continue
320
328
 
329
+ # 🐙 GitHub repo
321
330
  if resource.startswith("gh:"):
322
331
  resource_spec = resource[3:]
323
-
324
332
  if "@" in resource_spec:
325
333
  repo_id, ref = resource_spec.split("@", 1)
326
334
  else:
@@ -338,11 +346,15 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
338
346
  raise click.ClickException(
339
347
  f"❌ Failed to download GitHub repo {owner}/{name}@{ref or 'default'}: {e}"
340
348
  )
341
-
342
349
  local_paths.append(repo_path)
343
350
  continue
344
351
 
345
- # Handle normal relative or absolute URLs
352
+ # 🐳 Container registry support
353
+ if resource.startswith("cr:"):
354
+ install_from_cr(resource, internal=internal)
355
+ continue
356
+
357
+ # 🌐 Standard file or URL
346
358
  resource_url = urljoin(base_url, resource)
347
359
  local_path = download_file_from_url(
348
360
  url=resource_url,
@@ -357,6 +369,7 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
357
369
 
358
370
  return local_paths
359
371
 
372
+
360
373
  def selectable_resource_handler(metadata):
361
374
  selectable = metadata.get("selectable-resources")
362
375
  if not selectable:
@@ -0,0 +1,72 @@
1
+ import subprocess
2
+ import click
3
+
4
+ from sima_cli.utils.env import is_devkit_running_elxr
5
+
6
+ def update_elxr(version_or_url: str | None):
7
+ """
8
+ Update packages on an ELXR-based devkit using simaai-ota.
9
+ Uses InquirerPy for interactive menus.
10
+ """
11
+ # 1. Check ELXR system
12
+ if not is_devkit_running_elxr():
13
+ click.echo("ℹ️ Not an ELXR devkit, skipping update")
14
+ return
15
+
16
+ from InquirerPy import inquirer
17
+
18
+ # 2. Check network connectivity
19
+ if subprocess.call(["ping", "-c", "1", "deb.debian.org"],
20
+ stdout=subprocess.DEVNULL,
21
+ stderr=subprocess.DEVNULL) != 0:
22
+ click.echo("⚠️ ELXR devkit not connected to the network, skipping update")
23
+ return
24
+
25
+ # 3. Choose update path
26
+ if version_or_url is None:
27
+ choice = inquirer.select(
28
+ message="How would you like to update this ELXR devkit?",
29
+ choices=[
30
+ {"name": "Update all packages (no reinstall if up-to-date)", "value": "normal"},
31
+ {"name": "Update all packages (force reinstall)", "value": "force"},
32
+ {"name": "Update to a specific simaai-palette version", "value": "version"},
33
+ {"name": "Fix u-boot environment (force reinstall + overwrite)", "value": "fix-uboot"},
34
+ ],
35
+ default="normal"
36
+ ).execute()
37
+
38
+ if choice == "normal":
39
+ cmd = ["simaai-ota"]
40
+ desc = "Update all packages (no reinstall)"
41
+ elif choice == "force":
42
+ cmd = ["simaai-ota", "-f"]
43
+ desc = "Update all packages (force reinstall)"
44
+ elif choice == "version":
45
+ version = inquirer.text(
46
+ message="Enter simaai-palette version (e.g., git202510090802.8e94f9f-620):"
47
+ ).execute()
48
+ cmd = ["simaai-ota", "-v", version]
49
+ desc = f"Update to specific version {version}"
50
+ elif choice == "fix-uboot":
51
+ cmd = ["simaai-ota", "-f", "-o"]
52
+ desc = "Fix u-boot env (force reinstall + overwrite)"
53
+ else:
54
+ click.echo("❌ Invalid choice, aborting update")
55
+ return
56
+ else:
57
+ cmd = ["simaai-ota", "-v", version_or_url]
58
+ desc = f"Update to specific version {version_or_url}"
59
+
60
+ # 4. Execute
61
+ cmd = ["sudo"] + cmd
62
+ click.echo(f"➡️ {desc}\n " + click.style(f"Running: {' '.join(cmd)}", fg="cyan"))
63
+
64
+ # Check if passwordless sudo is available
65
+ if subprocess.call(["sudo", "-n", "true"],
66
+ stdout=subprocess.DEVNULL,
67
+ stderr=subprocess.DEVNULL) != 0:
68
+ click.echo("ℹ️ sudo may prompt you for a password...")
69
+
70
+ subprocess.check_call(cmd)
71
+ click.echo("✅ ELXR update completed successfully")
72
+
sima_cli/update/local.py CHANGED
@@ -5,7 +5,7 @@ import click
5
5
  import re
6
6
 
7
7
  from typing import Optional
8
- from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type
8
+ from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type, is_devkit_running_elxr
9
9
  from sima_cli.update.cleanlog import LineSquelcher
10
10
 
11
11
 
@@ -86,7 +86,7 @@ def get_local_board_info() -> Tuple[str, str, bool]:
86
86
  Retrieve the local board type and build version by reading /etc/build or /etc/buildinfo.
87
87
 
88
88
  Returns:
89
- (board_type, build_version, devkit_name, full_image): Tuple of strings, or ('', '') on failure.
89
+ (board_type, build_version, devkit_name, full_image, fwtype): Tuple of strings, or ('', '') on failure.
90
90
  """
91
91
  board_type = ""
92
92
  build_version = ""
@@ -108,8 +108,9 @@ def get_local_board_info() -> Tuple[str, str, bool]:
108
108
  continue
109
109
 
110
110
  devkit_name = get_exact_devkit_type()
111
+ fwtype = 'ELXR' if is_devkit_running_elxr() else 'Yocto'
111
112
 
112
- return board_type, build_version, devkit_name, is_board_running_full_image()
113
+ return board_type, build_version, devkit_name, is_board_running_full_image(), fwtype
113
114
 
114
115
 
115
116
  def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
@@ -1,6 +1,6 @@
1
1
  from sima_cli.update.updater import download_image
2
2
  from sima_cli.utils.net import get_local_ip_candidates
3
- from sima_cli.update.remote import get_remote_board_info, copy_file_to_remote_board, DEFAULT_PASSWORD, run_remote_command, init_ssh_session
3
+ from sima_cli.update.remote import wait_for_ssh, copy_file_to_remote_board, DEFAULT_PASSWORD, run_remote_command, init_ssh_session
4
4
  import os
5
5
  import platform
6
6
  import threading
@@ -29,6 +29,9 @@ def flash_emmc(client_manager, emmc_image_paths):
29
29
  if info.get("state") == "Connected"
30
30
  ]
31
31
 
32
+ # must comment out when checking in, this is for testing only
33
+ # clients = [("192.168.1.20", {"type": "devkit", "state": "Connected"})]
34
+
32
35
  if not clients:
33
36
  click.echo("📭 No connected clients available to flash.")
34
37
  return
@@ -75,7 +78,7 @@ def flash_emmc(client_manager, emmc_image_paths):
75
78
 
76
79
  # Step c: Decide flashing method
77
80
  wic_path = next((p for p in emmc_image_paths if p.endswith(".wic.gz")), None)
78
- img_path = next((p for p in emmc_image_paths if p.endswith(".img")), None)
81
+ img_path = next((p for p in emmc_image_paths if p.endswith(".img.gz")), None)
79
82
 
80
83
  if wic_path:
81
84
  filename = os.path.basename(wic_path)
@@ -90,7 +93,7 @@ def flash_emmc(client_manager, emmc_image_paths):
90
93
  elif img_path:
91
94
  filename = os.path.basename(img_path)
92
95
  remote_path = f"/tmp/{filename}"
93
- flash_cmd = f"sudo dd if={remote_path} of=/dev/mmcblk0 bs=4M conv=fsync status=progress"
96
+ flash_cmd = f"sudo gzip -dc {remote_path} | sudo dd of=/dev/mmcblk0 bs=16M status=progress"
94
97
  run_remote_command(ssh, flash_cmd)
95
98
  else:
96
99
  click.echo("❌ No .wic.gz or .img image found in emmc_image_paths.")
@@ -130,30 +133,32 @@ class ClientManager:
130
133
  ).start()
131
134
 
132
135
  def monitor_client(self, ip, start_time):
133
- """Monitor client by calling get_remote_board_info after 1 minutes, retry every 10 seconds."""
136
+ """
137
+ Monitor client connectivity by waiting for SSH availability on the target IP.
138
+ Uses `wait_for_ssh()` instead of retrieving board info.
139
+ Retries until success or shutdown_event is set.
140
+ """
134
141
  try:
135
- # Wait ~1 minute before first attempt
142
+ # Wait up to 1 minute after the start before first attempt
136
143
  if self.shutdown_event.wait(timeout=max(0, 60 - (time.time() - start_time))):
137
144
  return
138
145
 
139
146
  while not self.shutdown_event.is_set():
147
+ click.echo(f"🔍 Checking SSH availability for {ip}...")
140
148
  try:
141
- board_type, build_version, _, _ = get_remote_board_info(ip)
142
-
143
- if board_type and build_version:
144
- with self.lock:
145
- self.clients[ip]['state'] = 'Connected'
146
- self.clients[ip]['board_info'] = f"{board_type}|{build_version}"
147
- click.echo(f"✅ Board info retrieved for {ip}: {board_type} with {build_version}")
148
- break
149
- else:
150
- log.debug(f"Incomplete board info for {ip}: {board_type}, {build_version}")
149
+ wait_for_ssh(ip, timeout=120)
150
+ with self.lock:
151
+ self.clients[ip]['state'] = 'Connected'
152
+ self.clients[ip]['board_info'] = "SSH available"
153
+ click.echo(f"✅ SSH is available on {ip}")
154
+ break
151
155
 
152
156
  except Exception as e:
153
- log.info(f"get_remote_board_info failed for {ip}, retrying in 10s: {e}")
157
+ log.info(f"SSH not yet available for {ip}, retrying in 10s: {e}")
154
158
 
159
+ # Wait before retrying
155
160
  if self.shutdown_event.wait(timeout=10):
156
- break
161
+ break
157
162
 
158
163
  except Exception as e:
159
164
  log.error(f"Unexpected error while monitoring {ip}: {e}")
@@ -391,7 +396,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
391
396
  # Extract specific image paths
392
397
  wic_gz_file = next((f for f in file_list if f.endswith(".wic.gz")), None)
393
398
  bmap_file = next((f for f in file_list if f.endswith(".wic.bmap")), None)
394
- elxr_img_file = next((f for f in file_list if f.endswith(".img")), None)
399
+ elxr_img_file = next((f for f in file_list if f.endswith(".img.gz")), None)
395
400
  emmc_image_paths = [p for p in [wic_gz_file, bmap_file, elxr_img_file] if p]
396
401
 
397
402
  # Check global custom_rootfs before doing anything else
@@ -403,7 +408,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
403
408
  import glob
404
409
  wic_gz_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.gz"))), None)
405
410
  bmap_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.bmap"))), None)
406
- exlr_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.img"))), None)
411
+ exlr_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.img.gz"))), None)
407
412
 
408
413
  if not (wic_gz_file and bmap_file):
409
414
  raise RuntimeError(
sima_cli/update/query.py CHANGED
@@ -25,7 +25,10 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
25
25
  "path": {{
26
26
  "$match": "{fw_path}/*/artifacts/palette"
27
27
  }},
28
- "name": "modalix-tftp-boot.tar.gz",
28
+ "$or": [
29
+ {{"name": "modalix-tftp-boot-palette.tar.gz"}},
30
+ {{"name": "modalix-tftp-boot.tar.gz"}}
31
+ ],
29
32
  "type": "file"
30
33
  }}).include("repo", "path", "name")
31
34
  """.strip()
sima_cli/update/remote.py CHANGED
@@ -14,7 +14,7 @@ from sima_cli.update.cleanlog import LineSquelcher
14
14
  DEFAULT_USER = "sima"
15
15
  DEFAULT_PASSWORD = "edgeai"
16
16
 
17
- def _wait_for_ssh(ip: str, timeout: int = 120):
17
+ def wait_for_ssh(ip: str, timeout: int = 120):
18
18
  """
19
19
  Show an animated spinner while waiting for SSH on the target IP to become available.
20
20
 
@@ -54,22 +54,24 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
54
54
  print("\r✅ Board is online! \n")
55
55
 
56
56
 
57
- def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool]:
57
+ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool, str]:
58
58
  """
59
- Connect to the remote board and retrieve board type, build version, and devkit model.
59
+ Connect to the remote board and retrieve board type, build version,
60
+ devkit model, full_image flag, and fwtype (from DISTRO).
60
61
 
61
62
  Args:
62
63
  ip (str): IP address of the board.
63
64
  passwd (str): SSH password.
64
65
 
65
66
  Returns:
66
- (board_type, build_version, devkit_model, full_image):
67
- Tuple of strings + bool, or ('', '', '', False) on failure.
67
+ (board_type, build_version, devkit_model, full_image, fwtype):
68
+ Tuple of strings + bool, or ('', '', '', False, '') on failure.
68
69
  """
69
70
  board_type = ""
70
71
  build_version = ""
71
72
  devkit_model = ""
72
73
  full_image = False
74
+ fwtype = ""
73
75
 
74
76
  try:
75
77
  ssh = paramiko.SSHClient()
@@ -105,19 +107,29 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
105
107
  board_type = line.split("=", 1)[-1].strip()
106
108
  elif line.startswith("SIMA_BUILD_VERSION"):
107
109
  build_version = line.split("=", 1)[-1].strip()
110
+ elif line.startswith("DISTRO "):
111
+ fwtype = line.split("=", 1)[-1].strip()
108
112
 
109
- return board_type, build_version, devkit_model, full_image
113
+ return board_type, build_version, devkit_model, full_image, fwtype
110
114
 
111
115
  except Exception as e:
112
116
  click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
113
- return "", "", "", False
114
-
117
+ return "", "", "", False, ""
115
118
 
116
119
  def _scp_file(sftp, local_path: str, remote_path: str):
117
- """Upload file via SFTP and report."""
120
+ """Upload file via SFTP with tqdm progress bar."""
118
121
  filename = os.path.basename(local_path)
119
- click.echo(f"📤 Uploading {filename} → {remote_path}")
120
- sftp.put(local_path, remote_path)
122
+ total_size = os.path.getsize(local_path)
123
+
124
+ from tqdm import tqdm
125
+
126
+ with tqdm(total=total_size, unit='B', unit_scale=True,
127
+ desc=f"📤 {filename}", ascii=True) as pbar:
128
+ def progress(transferred, total):
129
+ pbar.update(transferred - pbar.n)
130
+
131
+ sftp.put(local_path, remote_path, callback=progress)
132
+
121
133
  click.echo("✅ Upload complete")
122
134
 
123
135
 
@@ -228,7 +240,7 @@ def run_remote_command_capture(ssh, command: str, password: str = DEFAULT_PASSWO
228
240
  return code, out, err
229
241
 
230
242
 
231
- def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> str | None:
243
+ def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> Optional[str]:
232
244
  """
233
245
  Determine the remote boot device: 'mmcblk0', 'mmcblk1', or None.
234
246
 
@@ -246,7 +258,9 @@ fi
246
258
  [ -n "$mmc" ] && printf '%s\n' "$mmc"
247
259
  """
248
260
 
249
- code, out, err = run_remote_command_capture(ssh, f"sh -c {quote_shell(remote_script)}", password=password)
261
+ code, out, err = run_remote_command_capture(
262
+ ssh, f"sh -c {quote_shell(remote_script)}", password=password
263
+ )
250
264
  mmc = out.strip()
251
265
  return mmc if mmc else None
252
266
 
@@ -257,20 +271,36 @@ def quote_shell(s: str) -> str:
257
271
 
258
272
  def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd: str):
259
273
  """
260
- Copy a file to the remote board over SSH.
274
+ Copy a file to the remote board over SSH with tqdm progress bar.
261
275
  Assumes default credentials: sima / edgeai.
262
276
  """
263
277
  ssh = paramiko.SSHClient()
264
278
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
279
+
280
+ from tqdm import tqdm
265
281
 
266
282
  try:
267
283
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
268
284
  sftp = ssh.open_sftp()
269
285
 
270
- # Upload the file
271
286
  base_file_path = os.path.basename(file_path)
272
- click.echo(f"📤 Uploading {file_path} → {remote_dir}")
273
- sftp.put(file_path, os.path.join(remote_dir, base_file_path))
287
+ remote_path = os.path.join(remote_dir, base_file_path)
288
+ file_size = os.path.getsize(file_path)
289
+
290
+ click.echo(f"📤 Uploading {file_path} → {remote_path}")
291
+
292
+ with tqdm(total=file_size, unit="B", unit_scale=True,
293
+ desc=f"📤 {base_file_path}", ncols=100) as pbar:
294
+
295
+ def progress(transferred, total):
296
+ pbar.update(transferred - pbar.n)
297
+
298
+ sftp.put(file_path, remote_path, callback=progress)
299
+
300
+ click.echo("✅ Upload complete")
301
+
302
+ sftp.close()
303
+ ssh.close()
274
304
  return True
275
305
 
276
306
  except Exception as e:
@@ -362,7 +392,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
362
392
 
363
393
  # Wait for board to come back
364
394
  time.sleep(5)
365
- _wait_for_ssh(ip, timeout=120)
395
+ wait_for_ssh(ip, timeout=120)
366
396
 
367
397
  # Reconnect and verify version
368
398
  try:
@@ -384,5 +414,5 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
384
414
 
385
415
 
386
416
  if __name__ == "__main__":
387
- _wait_for_ssh("192.168.2.20", timeout=60)
417
+ wait_for_ssh("192.168.2.20", timeout=60)
388
418
  print(get_remote_board_info("192.168.2.20"))
@@ -14,7 +14,8 @@ from sima_cli.download import download_file_from_url
14
14
  from sima_cli.utils.config_loader import load_resource_config
15
15
  from sima_cli.update.remote import push_and_update_remote_board, get_remote_board_info, reboot_remote_board
16
16
  from sima_cli.update.query import list_available_firmware_versions
17
- from sima_cli.utils.env import is_sima_board
17
+ from sima_cli.utils.env import is_sima_board, is_devkit_running_elxr
18
+ from sima_cli.update.elxr import update_elxr
18
19
 
19
20
  if is_sima_board():
20
21
  from sima_cli.update import local
@@ -65,8 +66,8 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
65
66
  image_file = 'release.tar.gz' if flavor == 'headless' else 'graphics.tar.gz'
66
67
  download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/{image_file}"
67
68
  elif swtype == 'elxr':
68
- image_file = f'{board}-tftp-boot.tar.gz'
69
- download_url = url.rstrip("/") + f"/soc-images/elxr/{board}/{version_or_url}/artifacts/palette/{image_file}"
69
+ image_file = f'{board}-tftp-boot-minimal.tar.gz'
70
+ download_url = url.rstrip("/") + f"/soc-images/elxr/{board}/{version_or_url}/artifacts/minimal/{image_file}"
70
71
 
71
72
  return download_url
72
73
 
@@ -204,7 +205,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
204
205
  f"{board}-dvt.dtb",
205
206
  f"{board}-hhhl_x16.dtb",
206
207
  f"pcie-4rc-2rc-2rc.dtbo",
207
- f"elxr-palette-{board}-arm64.cpio.gz",
208
+ f"elxr-minimal-{board}-arm64.cpio.gz",
208
209
  f"simaai-image-{_flavor}-{board}.wic.gz",
209
210
  f"simaai-image-{_flavor}-{board}.wic.bmap",
210
211
  f"simaai-image-{_flavor}-{board}.cpio.gz"
@@ -329,14 +330,15 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
329
330
  if internal and update_type == "netboot" and swtype == "elxr":
330
331
  base_url = os.path.dirname(image_url)
331
332
  base_version = version_or_url.split('_')[0]
332
- extra_files = [f"elxr-palette-{board}-{base_version}-arm64.img.gz"]
333
+ extra_files = [f"../palette/elxr-palette-{board}-{base_version}-arm64.img.gz"]
333
334
 
334
335
  for fname in extra_files:
335
336
  extra_url = f"{base_url}/{fname}"
336
337
  try:
337
338
  click.echo(f"📥 Downloading extra file: {fname} from {extra_url} saving into {dest_path}")
338
339
  netboot_file_path = download_file_from_url(extra_url, dest_path, internal=internal)
339
- extracted_files.extend(_extract_required_files(netboot_file_path, board, update_type, flavor))
340
+ print(netboot_file_path)
341
+ extracted_files.extend([netboot_file_path])
340
342
  click.echo(f"✅ Saved {fname} to {dest_path}")
341
343
  except Exception as e:
342
344
  click.echo(f"⚠️ Failed to download {fname}: {e}")
@@ -428,7 +430,7 @@ def _update_board(extracted_paths: List[str], board: str, passwd: str, flavor: s
428
430
  return
429
431
 
430
432
  # Optionally verify the board type
431
- board_type, _, _, full_image = get_local_board_info()
433
+ board_type, _, _, full_image, _ = get_local_board_info()
432
434
  if board_type.lower() != board.lower():
433
435
  click.echo(f"❌ Board mismatch: expected '{board}', but found '{board_type}'")
434
436
  return
@@ -465,7 +467,7 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
465
467
 
466
468
  # Get remote board info
467
469
  click.echo("🔍 Checking remote board type and version...")
468
- remote_board, remote_version, _, _ = get_remote_board_info(ip, passwd)
470
+ remote_board, remote_version, _, _, _ = get_remote_board_info(ip, passwd)
469
471
 
470
472
  if not remote_board:
471
473
  click.echo("❌ Could not determine remote board type.")
@@ -505,6 +507,7 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
505
507
  extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor, swtype=swtype)
506
508
  return extracted_paths
507
509
 
510
+
508
511
  def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'auto'):
509
512
  r"""
510
513
  Update the system based on environment and input.
@@ -529,19 +532,31 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
529
532
  click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
530
533
 
531
534
  if env_type == 'board':
532
- board, version, devkit_name, full_image = get_local_board_info()
535
+ board, version, devkit_name, full_image, fwtype = get_local_board_info()
533
536
  else:
534
- board, version, devkit_name, full_image = get_remote_board_info(ip, passwd)
537
+ board, version, devkit_name, full_image, fwtype = get_remote_board_info(ip, passwd)
535
538
 
536
539
  flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
537
540
 
538
541
  if board in ['davinci', 'modalix']:
539
- click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}")
542
+ click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}, firmware: {fwtype}")
540
543
 
541
544
  if flavor == 'full' and 'modalix' not in devkit_name:
542
545
  click.echo(f"❌ You've requested updating {devkit_name} to full image, this is only supported for the Modalix DevKit")
543
546
  return
544
547
 
548
+ if is_devkit_running_elxr():
549
+ return update_elxr(version_or_url)
550
+ elif fwtype.lower() == 'elxr':
551
+ click.echo(
552
+ click.style(
553
+ "⚠️ ELXR does not support remote update.\n"
554
+ " Please connect the DevKit to the Internet\n"
555
+ " and run: ", fg="yellow"
556
+ ) + click.style("sima-cli update", fg="cyan", bold=True)
557
+ )
558
+ return False
559
+
545
560
  # Davinci only supports headless build, so ignore the full flavor
546
561
  if board == 'davinci' and flavor != 'headless':
547
562
  click.echo(f"MLSoC only supports headless image, ignoring {flavor} setting")
@@ -0,0 +1,173 @@
1
+ import click
2
+ import subprocess
3
+
4
+ from sima_cli.utils.config import get_auth_token
5
+ from sima_cli.auth.basic_auth import resolve_public_registry, ensure_docker_available, docker_login_with_token
6
+
7
+ def _pull_container_from_registry(registry_url: str, image_ref: str) -> str:
8
+ """
9
+ Pulls container image from given registry and returns its local reference.
10
+ """
11
+ ensure_docker_available()
12
+ full_image = f"{registry_url.rstrip('/')}/{image_ref}"
13
+ subprocess.run(["docker", "pull", full_image], check=True)
14
+ return full_image
15
+
16
+
17
+ def docker_logout_from_registry(registry: str = "artifacts.eng.sima.ai"):
18
+ """
19
+ Logout from the specified Docker registry.
20
+ Removes stored credentials (even if managed by a credential helper).
21
+ Safe to call multiple times — no error if already logged out.
22
+ """
23
+ ensure_docker_available()
24
+ click.echo(f"🐳 Logging out of Docker registry")
25
+
26
+ try:
27
+ proc = subprocess.run(
28
+ ["docker", "logout", registry],
29
+ stdout=subprocess.PIPE,
30
+ stderr=subprocess.PIPE,
31
+ )
32
+
33
+ if proc.returncode == 0:
34
+ click.echo(proc.stdout.decode().strip() or f"✅ Logged out from {registry}")
35
+ else:
36
+ msg = proc.stderr.decode().strip() or proc.stdout.decode().strip()
37
+ if "not logged in" in msg.lower():
38
+ click.echo(f"ℹ️ Already logged out from {registry}")
39
+ else:
40
+ raise click.ClickException(f"Docker logout failed: {msg}")
41
+
42
+ except Exception as e:
43
+ raise click.ClickException(f"⚠️ Unexpected error during Docker logout: {e}")
44
+
45
+
46
+ def _select_artifactory_version(image_name: str) -> str:
47
+ """
48
+ Query available tags for an image from SiMa Artifactory and prompt user to select one.
49
+
50
+ Args:
51
+ image_name (str): The image name under sima-docker (e.g., 'modelsdk').
52
+
53
+ Returns:
54
+ str: The user-selected tag (e.g., 'latest_develop').
55
+
56
+ Raises:
57
+ click.ClickException: If no tags are found or query fails.
58
+ """
59
+ import requests
60
+ from InquirerPy import inquirer
61
+
62
+ click.echo(f"🔍 Querying available versions for {image_name} from Artifactory...")
63
+
64
+ # Retrieve internal auth token, if available
65
+ token = get_auth_token(internal=True)
66
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
67
+
68
+ if not token:
69
+ click.echo("⚠️ No internal token found; querying Artifactory anonymously.")
70
+
71
+ tags_url = (
72
+ f"https://artifacts.eng.sima.ai/artifactory/api/docker/"
73
+ f"sima-docker/v2/{image_name}/tags/list"
74
+ )
75
+
76
+ try:
77
+ resp = requests.get(tags_url, headers=headers, timeout=10)
78
+ if resp.status_code != 200:
79
+ raise click.ClickException(
80
+ f"❌ Failed to query tags for '{image_name}': {resp.status_code} {resp.text}"
81
+ )
82
+
83
+ tags = sorted(resp.json().get("tags", []))
84
+ if not tags:
85
+ raise click.ClickException(f"❌ No tags found for image '{image_name}'")
86
+
87
+ # Interactive tag selection
88
+ return inquirer.select(
89
+ message=f"Select a version for {image_name}:",
90
+ choices=tags,
91
+ default="latest" if "latest" in tags else tags[0],
92
+ ).execute()
93
+
94
+ except requests.exceptions.RequestException as e:
95
+ raise click.ClickException(f"❌ Network error while querying Artifactory: {e}")
96
+
97
+
98
+ def install_from_cr(resource_spec: str, internal: bool = False) -> str:
99
+ """
100
+ Install a component from a container registry resource.
101
+
102
+ Args:
103
+ resource_spec (str): Resource string in the form:
104
+ cr:<image>[:tag] or cr:<image>@<digest>
105
+ internal (bool): Whether to use SiMa internal Artifactory registry.
106
+
107
+ Examples:
108
+ install_from_cr("cr:modelsdk:latest_develop", internal=True)
109
+ install_from_cr("cr:modelsdk@sha256:abcd1234", internal=False)
110
+ """
111
+ if not ensure_docker_available():
112
+ click.echo("⚠️ Docker not available; skipping container installation.")
113
+ return ""
114
+
115
+ resource_spec = resource_spec[3:].strip()
116
+
117
+ # Parse image and version/digest
118
+ if "@" in resource_spec:
119
+ image_name, version = resource_spec.split("@", 1)
120
+ separator = "@"
121
+ elif ":" in resource_spec:
122
+ image_name, version = resource_spec.split(":", 1)
123
+ separator = ":"
124
+ else:
125
+ image_name, version, separator = resource_spec, None, ":"
126
+
127
+ # Resolve registry, default to Artifactory, if external resolve again.
128
+ registry_url = "artifacts.eng.sima.ai/sima-docker"
129
+
130
+ if not internal:
131
+ try:
132
+ token, registry_url = resolve_public_registry("ecr")
133
+ if not token or not registry_url:
134
+ click.secho("⚠️ Failed to resolve container registry or token is missing.", fg="yellow")
135
+ return None
136
+
137
+ success = docker_login_with_token("sima_cli", token, registry_url)
138
+ if success:
139
+ crtype = 'internal' if internal else 'SiMa.ai'
140
+ click.secho(f"✅ Logged in to {crtype} container registry", fg="green")
141
+ else:
142
+ click.secho(f"❌ Docker login to container registry failed", fg="red")
143
+
144
+ except Exception as e:
145
+ click.secho(f"❌ Unexpected error during container login: {e}", fg="red")
146
+ return None
147
+
148
+ # If internal and version not specified, prompt for version
149
+ if internal and version is None:
150
+ version = _select_artifactory_version(image_name)
151
+
152
+ # Compose final ref
153
+ full_image_ref = f"{registry_url}/{image_name}{separator}{version or 'latest'}"
154
+
155
+ # Auto-login if internal and not logged in
156
+ if internal and not get_auth_token(internal=internal):
157
+ click.echo(
158
+ f"⚠️ No internal token found; please login as "
159
+ + click.style("sima-cli -i login", fg="cyan", bold=True)
160
+ )
161
+ return
162
+
163
+ # Pull image
164
+ try:
165
+ registry_url = registry_url.replace('https://', '')
166
+ pulled_ref = _pull_container_from_registry(
167
+ registry_url, f"{image_name}{separator}{version or 'latest'}"
168
+ )
169
+ click.echo(f"✅ Successfully pulled container: {pulled_ref}")
170
+ except subprocess.CalledProcessError as e:
171
+ raise click.ClickException(f"❌ Docker pull failed: {e}")
172
+
173
+ return full_image_ref
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.39
3
+ Version: 0.0.40
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
@@ -1,12 +1,12 @@
1
1
  sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
2
2
  sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
3
- sima_cli/__version__.py,sha256=1TIyHW8Bk0vixt9Nebb4dCiN94FOO3JEiRQVppoIYuM,49
4
- sima_cli/cli.py,sha256=jGufO70y0NGLsIfEhr1pQNdFoBdzMxTO1R7p386d4-8,19291
3
+ sima_cli/__version__.py,sha256=PggPNN98qAKN0lJrYRuU2O1ULuwqDoUsrM7NUyq1sBw,49
4
+ sima_cli/cli.py,sha256=hE2NJfpBo5dDH_D9qQgvbn63Xw6R0i5uWluIkXrw2yg,19946
5
5
  sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  sima_cli/app_zoo/app.py,sha256=6u3iIqVZkuMK49kK0f3dVJCCf5-qC-xzLOS78-TkeN8,15738
7
7
  sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sima_cli/auth/basic_auth.py,sha256=UMEXCCnNQpjpp6RZxREs6iiKtYyaeqZBnTSR0wA8s6Q,8767
9
- sima_cli/auth/login.py,sha256=nE-dSHK_husXw1XJaEcOe3I1_bnwHkLgO_BqKuQODDM,3781
8
+ sima_cli/auth/basic_auth.py,sha256=nW-Q9y4dZTgq7FYuCwcUZstQT7rq-H-PJOolW1yo_lw,14212
9
+ sima_cli/auth/login.py,sha256=7le0had8uW-v__aEL1gl_bL9DZKJLxZmEoVI86YNmFo,2862
10
10
  sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
11
11
  sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
12
12
  sima_cli/download/__init__.py,sha256=6y4O2FOCYFR2jdnQoVi3hRtEoZ0Gw6rydlTy1SGJ5FE,218
@@ -14,7 +14,7 @@ sima_cli/download/downloader.py,sha256=UQdrBWLQsPQygaoVaufOjbzWmRoNnk6pgLdnbnVi0
14
14
  sima_cli/install/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  sima_cli/install/hostdriver.py,sha256=kAWDLebs60mbWIyTbUxmNrChcKW1uD5r7FtWNSUVUE4,5852
16
16
  sima_cli/install/metadata_info.py,sha256=wmMqwzGfXbuilkqaxRVrFOzOtTOiONkmPCyA2oDAQpA,2168
17
- sima_cli/install/metadata_installer.py,sha256=-hgMSF_1awwnnibzUqoMv4_GC7IUavu11eqetBVJZHE,31390
17
+ sima_cli/install/metadata_installer.py,sha256=N9QDbTIv__xhHmr33L1asaqUumaBASRr_7DnNEs3d8o,31943
18
18
  sima_cli/install/metadata_validator.py,sha256=Xp51J68hMvpQeX-11kPLgz1YarQZ2m-95gGpfe3-D3Q,5644
19
19
  sima_cli/install/optiview.py,sha256=r4DYdQDTUbZVCR87hl5T21gsjZrhqpU8hWnYxKmUJ_k,4790
20
20
  sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3930
@@ -32,21 +32,23 @@ sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,6
32
32
  sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
33
33
  sima_cli/update/bootimg.py,sha256=OA_GyZwI8dbU3kaucKmoxAZbnnSnjXeOkU6yuDPji1k,13632
34
34
  sima_cli/update/cleanlog.py,sha256=-V6eDl3MdsvDmCfkKUJTqkXJ_WnLJE01uxS7z96b15g,909
35
- sima_cli/update/local.py,sha256=z3oRk6JH-zbCdoS3JpgeW_ZB4kolG7nPhLC55A2yssk,5597
36
- sima_cli/update/netboot.py,sha256=V0QiqyHzAdrucJ87Q3Xlm2EBvkpjpu4RwPRWw9NXRcs,20334
37
- sima_cli/update/query.py,sha256=6RgvQfQT1_EtBGcibvVcz003dRKOq17NaGgL2mhaBbY,4891
38
- sima_cli/update/remote.py,sha256=wC4MSBQVxmibxtPBchAzFMhZYcRjxTiLlPSzVI0en4o,14690
39
- sima_cli/update/updater.py,sha256=CYOXTV-8zIo92Lv2vyBzKtUyrtFBwkJiknttlS-UxF4,25648
35
+ sima_cli/update/elxr.py,sha256=3mMKeu9AWi3zGY8q2bkJFQOVyacSrPXdKBJFre_BLcE,2776
36
+ sima_cli/update/local.py,sha256=8HK5j-5HEOC0nPWKkcPPnAxB3Ai8QpH3hGyucN0izXY,5698
37
+ sima_cli/update/netboot.py,sha256=gHu3Ts23Vlbvcn9dZ2u0KckP8y4WSmlzZCYWBiRsrmQ,20447
38
+ sima_cli/update/query.py,sha256=aqBeVYOTmy3gBVaxfK2c5f6_DH8_y32ZEB1INiJbOE8,5034
39
+ sima_cli/update/remote.py,sha256=ppxKGz8qBZ7rU_XEdXi6ouxmpidNx-5S9CNmAqrBpfU,15595
40
+ sima_cli/update/updater.py,sha256=Xhmpo41USPZMok8KwTEvYU6BIWv49kYC3zs_ls1LRrg,26325
40
41
  sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
42
  sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
42
43
  sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
43
44
  sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
45
+ sima_cli/utils/container_registries.py,sha256=hGS9cRVunzPRsktspaj3AnilFhcoGvimpcfvfg-bLS8,6341
44
46
  sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
45
47
  sima_cli/utils/env.py,sha256=0qY1-PJiI1G3uDVv774aimPXhHQBtY56FqXSMeMQTps,10401
46
48
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
47
49
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
48
50
  sima_cli/utils/pkg_update_check.py,sha256=GD0XVS2_l2WpuzL81TKkEQIDHXXffJeF3LELefncHNM,3467
49
- sima_cli-0.0.39.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
51
+ sima_cli-0.0.40.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
50
52
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
53
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
54
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -55,8 +57,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
55
57
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
58
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
59
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- sima_cli-0.0.39.dist-info/METADATA,sha256=GyCe69CeeFe8gcAtM0_ZS5eILLMMtJemMqnQPz_N9Z8,3705
59
- sima_cli-0.0.39.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
- sima_cli-0.0.39.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
61
- sima_cli-0.0.39.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
62
- sima_cli-0.0.39.dist-info/RECORD,,
60
+ sima_cli-0.0.40.dist-info/METADATA,sha256=X2jDLmN4ELyJ0HPPTAlnEiALlaB_OgrI_jS_COv5zM0,3705
61
+ sima_cli-0.0.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ sima_cli-0.0.40.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
63
+ sima_cli-0.0.40.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
64
+ sima_cli-0.0.40.dist-info/RECORD,,