sima-cli 0.0.34__py3-none-any.whl → 0.0.36__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.34"
2
+ __version__ = "0.0.36"
@@ -121,7 +121,6 @@ def login_external():
121
121
  session.headers["X-CSRF-Token"] = csrf_token
122
122
 
123
123
  if _is_session_valid(session):
124
- click.echo("🚀 You are already logged in.")
125
124
  return session
126
125
 
127
126
  # Fresh login prompt
sima_cli/auth/login.py CHANGED
@@ -60,15 +60,15 @@ def login_internal():
60
60
 
61
61
  click.echo(f"✅ Identity token is valid")
62
62
 
63
- # Step 2: Exchange for a short-lived access token (default: 7 days)
64
- access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=1209600)
63
+ # Step 2: Exchange for a short-lived access token (default: 30 days)
64
+ access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=2592000)
65
65
 
66
66
  if not access_token:
67
67
  return click.echo("❌ Failed to acquire short-lived access token.")
68
68
 
69
69
  # Step 3: Save token to internal auth config
70
70
  set_auth_token(access_token, internal=True)
71
- click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 14 days).")
71
+ click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 30 days).")
72
72
 
73
73
 
74
74
  def _login_external():
sima_cli/cli.py CHANGED
@@ -151,7 +151,7 @@ def download(ctx, url, dest):
151
151
  default='edgeai',
152
152
  help="Optional SSH password for remote board (default is 'edgeai')."
153
153
  )
154
- @click.option("-f", "--flavor", type=click.Choice(["headless", "full"], case_sensitive=False), default="headless", show_default=True, help="firmware flavor, full image supports NVMe, GUI on Modalix DevKit.")
154
+ @click.option("-f", "--flavor", type=click.Choice(["headless", "full", "auto"], case_sensitive=False), default="auto", show_default=True, help="firmware flavor, full image supports NVMe, GUI on Modalix DevKit.")
155
155
  @click.pass_context
156
156
  def update(ctx, version_or_url, ip, yes, passwd, flavor):
157
157
  """
@@ -313,9 +313,7 @@ def install_cmd(ctx, component, version, mirror, tag):
313
313
  if component:
314
314
  click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
315
315
  click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
316
- if install_from_metadata(metadata_url=mirror, internal=internal):
317
- click.echo("✅ Installation complete.")
318
- return
316
+ return install_from_metadata(metadata_url=mirror, internal=internal)
319
317
 
320
318
  # No component and no metadata: error
321
319
  if not component:
@@ -324,6 +322,10 @@ def install_cmd(ctx, component, version, mirror, tag):
324
322
 
325
323
  component = component.lower()
326
324
 
325
+ # if user specified gh: as component, treat it the same as -m
326
+ if component.startswith("gh:"):
327
+ return install_from_metadata(metadata_url=component, internal=False)
328
+
327
329
  # Validate version requirement
328
330
  if component in SDK_DEPENDENT_COMPONENTS and not version:
329
331
  click.echo(f"❌ The component '{component}' requires a specific SDK version. Please provide one using -v.")
@@ -135,6 +135,38 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
135
135
 
136
136
  return dest_path
137
137
 
138
+ def check_url_available(url: str, internal: bool = False) -> bool:
139
+ """
140
+ Perform a HEAD request to check if a resource is available.
141
+
142
+ Args:
143
+ url (str): The full URL to check.
144
+ internal (bool): Whether this is an internal resource on Artifactory.
145
+
146
+ Returns:
147
+ bool: True if the resource is available (status 200–399), False otherwise.
148
+ """
149
+ headers = {}
150
+ try:
151
+ if internal:
152
+ auth_token = get_auth_token(internal=True)
153
+ if auth_token:
154
+ headers["Authorization"] = f"Bearer {auth_token}"
155
+ head_fn = requests.head
156
+ elif 'https://docs.sima.ai' in url:
157
+ session = login_external()
158
+ head_fn = session.head
159
+ else:
160
+ session = requests.Session()
161
+ head_fn = session.head
162
+
163
+ resp = head_fn(url, headers=headers, timeout=10, allow_redirects=True)
164
+ # Consider any 2xx or 3xx as "available"
165
+ return 200 <= resp.status_code < 400
166
+
167
+ except Exception as e:
168
+ print(f"⚠️ HEAD check failed for {url}: {e}")
169
+ return False
138
170
 
139
171
  def download_folder_from_url(url: str, dest_folder: str = ".", internal: bool = False) -> List[str]:
140
172
  """
@@ -9,13 +9,13 @@ import tarfile
9
9
  import zipfile
10
10
  import stat
11
11
  from urllib.parse import urlparse
12
- from InquirerPy import inquirer
13
12
 
14
13
  from typing import Dict
15
14
  from tqdm import tqdm
16
15
  from urllib.parse import urljoin
17
16
  from pathlib import Path
18
17
  import subprocess
18
+ import requests
19
19
 
20
20
  from rich.console import Console
21
21
  from rich.panel import Panel
@@ -30,6 +30,153 @@ from sima_cli.install.metadata_info import print_metadata_summary, parse_size_st
30
30
 
31
31
  console = Console()
32
32
 
33
+ def _copy_dir(src: Path, dest: Path, label: str):
34
+ """
35
+ Copy files from src → dest, merging with existing files (no deletion).
36
+ Ensures that all parent directories for dest are created.
37
+ """
38
+ if not src.exists():
39
+ raise FileNotFoundError(f"SDK {label} not found: {src}")
40
+
41
+ # Ensure full path exists
42
+ dest.mkdir(parents=True, exist_ok=True)
43
+
44
+ # Copy tree correctly
45
+ for item in src.iterdir():
46
+ target = dest / item.name
47
+ if item.is_dir():
48
+ shutil.copytree(item, target, dirs_exist_ok=True)
49
+ else:
50
+ shutil.copy2(item, target)
51
+
52
+ click.echo(f"✅ Copied {label} into {dest}")
53
+
54
+ def _prepare_pipeline_project(repo_dir: Path):
55
+ """
56
+ Prepare a pipeline project by copying required SDK sources into the repo.
57
+
58
+ Steps:
59
+ 1. Copy core sources into the project folder
60
+ 2. Parse .project/pluginsInfo
61
+ 3. Copy required plugin sources from the SDK plugin zoo
62
+ """
63
+ plugins_info_file = repo_dir / ".project" / "pluginsInfo.json"
64
+ if not plugins_info_file.exists():
65
+ return
66
+
67
+ click.echo("📦 Preparing pipeline project...")
68
+
69
+ try:
70
+ data = json.loads(plugins_info_file.read_text())
71
+ plugins = data.get("pluginsInfo", [])
72
+ except Exception as e:
73
+ raise RuntimeError(f"Failed to read {plugins_info_file}: {e}")
74
+
75
+ # Step a: copy core
76
+ # Define what to copy
77
+ copy_map = [
78
+ (
79
+ Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/core"),
80
+ repo_dir / "core",
81
+ "core"
82
+ ),
83
+ (
84
+ Path("/usr/local/simaai/utils/gst_app"),
85
+ repo_dir / "dependencies" / "gst_app",
86
+ "dependencies/gst_app"
87
+ ),
88
+ (
89
+ Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/gst/templates"),
90
+ repo_dir / "plugins" / "templates",
91
+ "plugins/templates"
92
+ ),
93
+ ]
94
+
95
+ # Execute
96
+ for src, dest, label in copy_map:
97
+ _copy_dir(src, dest, label)
98
+
99
+ # Step b/c: scan plugin paths and copy SDK plugins
100
+ sdk_plugins_base = Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/gst")
101
+ dest_plugins_dir = repo_dir / "plugins"
102
+ dest_plugins_dir.mkdir(exist_ok=True)
103
+
104
+ for plugin in plugins:
105
+ try:
106
+ path = plugin.get("path", "")
107
+ if not path:
108
+ continue
109
+ parts = path.split("/")
110
+ if len(parts) < 2:
111
+ continue
112
+
113
+ plugin_name = parts[1]
114
+ sdk_plugin_path = sdk_plugins_base / plugin_name
115
+ if not sdk_plugin_path.exists():
116
+ click.echo(
117
+ f"⚠️ Missing plugin source: {plugin_name} in the SDK, skipping. "
118
+ "It's possible that this is a custom plugin already exists in the repo"
119
+ )
120
+ continue
121
+
122
+ dest_plugin_path = dest_plugins_dir / plugin_name
123
+ dest_plugin_path.mkdir(parents=True, exist_ok=True)
124
+
125
+ # Merge instead of deleting
126
+ shutil.copytree(sdk_plugin_path, dest_plugin_path, dirs_exist_ok=True)
127
+
128
+ click.echo(f"✅ Copied plugin {plugin_name} into {dest_plugin_path}")
129
+
130
+ except Exception as e:
131
+ click.echo(f"❌ Error copying plugin {plugin}: {e}")
132
+
133
+ click.echo("🎉 Pipeline project prepared.")
134
+
135
+ def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, token: str = None) -> str:
136
+ """
137
+ Download and extract a GitHub repo tarball via the REST API (no git required).
138
+
139
+ Args:
140
+ owner (str): GitHub org/user
141
+ repo (str): Repo name
142
+ ref (str): Branch, tag, or commit (default = default branch)
143
+ dest_folder (str): Where to extract
144
+ token (str): Optional GitHub token for private repos
145
+
146
+ Returns:
147
+ str: Path to the extracted repo
148
+ """
149
+ url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}" if ref else f"https://api.github.com/repos/{owner}/{repo}/tarball"
150
+ headers = {}
151
+ if token:
152
+ headers["Authorization"] = f"Bearer {token}"
153
+
154
+ click.echo(f"🐙 Downloading GitHub repo: {owner}/{repo}" + (f"@{ref}" if ref else ""))
155
+
156
+ with requests.get(url, headers=headers, stream=True) as r:
157
+ if r.status_code in (401, 403):
158
+ raise PermissionError("Authentication required for GitHub repo")
159
+ r.raise_for_status()
160
+
161
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as tmp_file:
162
+ for chunk in r.iter_content(chunk_size=8192):
163
+ tmp_file.write(chunk)
164
+ tmp_path = Path(tmp_file.name)
165
+
166
+ repo_dir = Path(dest_folder) / repo
167
+ repo_dir.mkdir(parents=True, exist_ok=True)
168
+
169
+ _extract_tar_strip_top_level(tmp_path, repo_dir)
170
+ tmp_path.unlink(missing_ok=True)
171
+ click.echo(f"✅ Downloaded GitHub repo to folder: {repo_dir}")
172
+
173
+ try:
174
+ _prepare_pipeline_project(repo_dir)
175
+ except Exception as e:
176
+ click.echo(f"⚠️ Pipeline preparation skipped: {e}")
177
+
178
+ return str(repo_dir)
179
+
33
180
  def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal: bool = False, skip_models: bool = False) -> list:
34
181
  """
35
182
  Downloads resources defined in metadata to a local destination folder.
@@ -85,11 +232,34 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
85
232
  model_path = snapshot_download(
86
233
  repo_id=repo_id,
87
234
  local_dir=target_dir,
88
- revision=revision # None if not specified
235
+ local_dir_use_symlinks=False
89
236
  )
90
237
  local_paths.append(model_path)
91
238
  continue
92
239
 
240
+ if resource.startswith("gh:"):
241
+ resource_spec = resource[3:]
242
+ if "@" in resource_spec:
243
+ repo_id, ref = resource_spec.split("@", 1)
244
+ else:
245
+ repo_id, ref = resource_spec, None
246
+
247
+ if "/" not in repo_id:
248
+ raise click.ClickException(f"❌ Invalid GitHub repo spec: {resource}")
249
+
250
+ owner, name = repo_id.split("/", 1)
251
+
252
+ try:
253
+ token = os.getenv("GITHUB_TOKEN", None)
254
+ repo_path = _download_github_repo(owner, name, ref, dest_folder, token)
255
+ except Exception as e:
256
+ raise click.ClickException(
257
+ f"❌ Failed to download GitHub repo {owner}/{name}@{ref or 'default'}: {e}"
258
+ )
259
+
260
+ local_paths.append(repo_path)
261
+ continue
262
+
93
263
  # Handle normal relative or absolute URLs
94
264
  resource_url = urljoin(base_url, resource)
95
265
  local_path = download_file_from_url(
@@ -113,6 +283,8 @@ def selectable_resource_handler(metadata):
113
283
  choices = [(f"{i.get('name','Unnamed')} ({i.get('url','')})" if i.get('url') else i.get('name','Unnamed')) for i in selectable]
114
284
  choices.append("Skip")
115
285
 
286
+ from InquirerPy import inquirer
287
+
116
288
  sel = inquirer.select(message="Select an opt-in resource to download:", choices=choices).execute()
117
289
  if sel == "Skip":
118
290
  print("✅ No selectable resource chosen.")
@@ -295,6 +467,25 @@ def _extract_zip_streaming(zip_path: Path, extract_dir: Path, overwrite: bool =
295
467
 
296
468
  print(f"✅ Extracted {len(members)} entries to {extract_dir}/")
297
469
 
470
+ def _extract_tar_strip_top_level(tar_path: Path, extract_dir: Path):
471
+ """Extract a GitHub tarball, stripping the top-level folder."""
472
+ with tarfile.open(tar_path, "r:*") as tar:
473
+ members = tar.getmembers()
474
+
475
+ # Detect top-level prefix (first part before '/')
476
+ top_level = None
477
+ if members:
478
+ first_name = members[0].name
479
+ top_level = first_name.split("/", 1)[0]
480
+
481
+ for member in members:
482
+ # Strip top-level folder
483
+ if top_level and member.name.startswith(top_level + "/"):
484
+ member.name = member.name[len(top_level) + 1 :]
485
+ if not member.name:
486
+ continue
487
+ tar.extract(member, path=extract_dir)
488
+
298
489
  def _combine_multipart_files(folder: str):
299
490
  """
300
491
  Scan a folder for multipart files like name-split-aa, -ab, etc.,
@@ -352,20 +543,27 @@ def _combine_multipart_files(folder: str):
352
543
 
353
544
  print(f"✅ Extracted to: {extract_dir}/")
354
545
 
355
- def _extract_archives_in_folder(folder: str):
546
+ def _extract_archives_in_folder(folder: str, local_paths):
356
547
  """
357
- Extract all .tar.gz and .zip files in the given folder into subdirectories.
548
+ Extract .tar, .gz, .tar.gz, and .zip files in the given folder,
549
+ but only if they are listed in local_paths.
358
550
  Uses streaming to avoid NFS performance issues.
359
551
  """
360
552
  folder = Path(folder)
553
+ local_paths = {str(Path(p).resolve()) for p in local_paths}
554
+
361
555
  for file in folder.iterdir():
362
556
  if not file.is_file():
363
557
  continue
364
558
 
365
- # TAR.GZ
366
- if file.suffixes == [".tar", ".gz"] or file.name.endswith(".tar.gz"):
559
+ file_resolved = str(file.resolve())
560
+ if file_resolved not in local_paths:
561
+ continue
562
+
563
+ # TAR, GZ, TAR.GZ → all handled by _extract_tar_streaming
564
+ if file.suffix in [".tar", ".gz"] or file.name.endswith(".tar.gz"):
367
565
  extract_dir = folder / file.stem.replace(".tar", "")
368
- print(f"📦 Extracting TAR.GZ: {file.name} to {extract_dir}/")
566
+ print(f"📦 Extracting TAR/GZ: {file.name} to {extract_dir}/")
369
567
  _extract_tar_streaming(file, extract_dir)
370
568
 
371
569
  # ZIP
@@ -470,8 +668,45 @@ def _run_installation_script(metadata: Dict, extract_path: str = "."):
470
668
 
471
669
  print("✅ Installation completed successfully.")
472
670
 
671
+ def _resolve_github_metadata_url(gh_ref: str) -> str:
672
+ """
673
+ Resolve a GitHub shorthand like gh:org/repo@tag into a raw URL for metadata.json.
674
+ If tag is omitted, defaults to 'main'.
675
+ """
676
+ try:
677
+ _, repo_ref = gh_ref.split(":", 1) # remove 'gh:'
678
+ if "@" in repo_ref:
679
+ org_repo, tag = repo_ref.split("@", 1)
680
+ else:
681
+ org_repo, tag = repo_ref, "main"
682
+
683
+ owner, repo = org_repo.split("/", 1)
684
+ token = os.getenv("GITHUB_TOKEN")
685
+
686
+ # Use GitHub API to fetch the metadata.json file
687
+ api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/metadata.json?ref={tag}"
688
+ headers = {"Accept": "application/vnd.github.v3.raw"}
689
+ if token:
690
+ headers["Authorization"] = f"token {token}"
691
+
692
+ r = requests.get(api_url, headers=headers)
693
+ r.raise_for_status()
694
+
695
+ # Write metadata.json locally so downstream logic can use a file/URL
696
+ local_path = os.path.join("/tmp", f"{repo}-{tag}-metadata.json")
697
+ with open(local_path, "wb") as f:
698
+ f.write(r.content)
699
+
700
+ return local_path
701
+ except Exception as e:
702
+ raise RuntimeError(f"Failed to resolve GitHub metadata URL {gh_ref}: {e}")
703
+
473
704
  def install_from_metadata(metadata_url: str, internal: bool, install_dir: str = '.'):
474
705
  try:
706
+ if metadata_url.startswith("gh:"):
707
+ metadata_url = _resolve_github_metadata_url(metadata_url)
708
+ internal = False
709
+
475
710
  metadata, _ = _download_and_validate_metadata(metadata_url, internal)
476
711
  print_metadata_summary(metadata=metadata)
477
712
 
@@ -480,7 +715,7 @@ def install_from_metadata(metadata_url: str, internal: bool, install_dir: str =
480
715
  local_paths = _download_assets(metadata, metadata_url, install_dir, internal)
481
716
  if len(local_paths) > 0:
482
717
  _combine_multipart_files(install_dir)
483
- _extract_archives_in_folder(install_dir)
718
+ _extract_archives_in_folder(install_dir, local_paths)
484
719
  _run_installation_script(metadata=metadata, extract_path=install_dir)
485
720
 
486
721
  except Exception as e:
@@ -1,4 +1,3 @@
1
- from InquirerPy import inquirer
2
1
  import subprocess
3
2
  import os
4
3
  import re
@@ -144,6 +143,7 @@ def set_default_route(iface, ip):
144
143
  check=True
145
144
  )
146
145
  print(f"✅ Default route set via {iface} ({gateway})")
146
+
147
147
  except subprocess.CalledProcessError:
148
148
  print(f"❌ Failed to set default route via {iface}")
149
149
 
@@ -152,6 +152,10 @@ def network_menu():
152
152
  print("❌ This command only runs on the DevKit")
153
153
  return
154
154
 
155
+ from InquirerPy import inquirer
156
+
157
+ print("✅ Scanning network configuration, please wait...")
158
+
155
159
  while True:
156
160
  interfaces = get_interfaces()
157
161
  choices = ["🚪 Quit Menu"]
@@ -165,6 +169,8 @@ def network_menu():
165
169
  iface_map[label] = iface
166
170
 
167
171
  try:
172
+
173
+
168
174
  iface_choice = inquirer.fuzzy(
169
175
  message="Select Ethernet Interface:",
170
176
  choices=choices,
sima_cli/update/local.py CHANGED
@@ -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, fdt_name, full_image): Tuple of strings, or ('', '') on failure.
89
+ (board_type, build_version, devkit_name, full_image): Tuple of strings, or ('', '') on failure.
90
90
  """
91
91
  board_type = ""
92
92
  build_version = ""
@@ -107,9 +107,9 @@ def get_local_board_info() -> Tuple[str, str, bool]:
107
107
  except Exception:
108
108
  continue
109
109
 
110
- fdt_name = get_exact_devkit_type()
110
+ devkit_name = get_exact_devkit_type()
111
111
 
112
- return board_type, build_version, fdt_name, is_board_running_full_image()
112
+ return board_type, build_version, devkit_name, is_board_running_full_image()
113
113
 
114
114
 
115
115
  def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
sima_cli/update/remote.py CHANGED
@@ -53,19 +53,22 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
53
53
  else:
54
54
  print("\r✅ Board is online! \n")
55
55
 
56
- def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str]:
56
+
57
+ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool]:
57
58
  """
58
- Connect to the remote board and retrieve board type, build version, and fdt_name.
59
+ Connect to the remote board and retrieve board type, build version, and devkit model.
59
60
 
60
61
  Args:
61
62
  ip (str): IP address of the board.
63
+ passwd (str): SSH password.
62
64
 
63
65
  Returns:
64
- (board_type, build_version, fdt_name, full_image): Tuple of strings, or ('', '', '') on failure.
66
+ (board_type, build_version, devkit_model, full_image):
67
+ Tuple of strings + bool, or ('', '', '', False) on failure.
65
68
  """
66
69
  board_type = ""
67
70
  build_version = ""
68
- fdt_name = ""
71
+ devkit_model = ""
69
72
  full_image = False
70
73
 
71
74
  try:
@@ -77,12 +80,17 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
77
80
  _, stdout, _ = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
78
81
  build_output = stdout.read().decode()
79
82
 
80
- # Retrieve fdt_name from fw_printenv
81
- _, stdout, _ = ssh.exec_command("fw_printenv fdt_name 2>/dev/null")
82
- fdt_output = stdout.read().decode()
83
-
84
- # 3) NVMe presence (ensure sbin is in PATH for non-root shells)
85
- # Note: 'command -v' is POSIX; 'which' as fallback on some busybox setups.
83
+ # Retrieve model from /proc/device-tree/model
84
+ _, stdout, _ = ssh.exec_command("cat /proc/device-tree/model 2>/dev/null || echo ''")
85
+ model_output = stdout.read().decode().strip()
86
+ if model_output:
87
+ # Normalize string: remove "SiMa.ai " prefix and trailing "Board"
88
+ if model_output.startswith("SiMa.ai "):
89
+ model_output = model_output[len("SiMa.ai "):]
90
+ model_output = model_output.replace(" Board", "")
91
+ devkit_model = model_output.lower().replace(" ", "-")
92
+
93
+ # Check for NVMe tool presence (determine if full image is flashed)
86
94
  nvme_check_cmd = r'PATH="$PATH:/usr/sbin:/sbin"; command -v nvme >/dev/null 2>&1 || which nvme >/dev/null 2>&1; echo $?'
87
95
  _, stdout, _ = ssh.exec_command(nvme_check_cmd)
88
96
  nvme_rc = stdout.read().decode().strip()
@@ -90,6 +98,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
90
98
 
91
99
  ssh.close()
92
100
 
101
+ # Parse build info
93
102
  for line in build_output.splitlines():
94
103
  line = line.strip()
95
104
  if line.startswith("MACHINE"):
@@ -97,11 +106,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
97
106
  elif line.startswith("SIMA_BUILD_VERSION"):
98
107
  build_version = line.split("=", 1)[-1].strip()
99
108
 
100
- for line in fdt_output.splitlines():
101
- if line.startswith("fdt_name"):
102
- fdt_name = line.split("=", 1)[-1].strip().replace('.dtb', '')
103
-
104
- return board_type, build_version, fdt_name, full_image
109
+ return board_type, build_version, devkit_model, full_image
105
110
 
106
111
  except Exception as e:
107
112
  click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
@@ -7,7 +7,6 @@ import tarfile
7
7
  import gzip
8
8
  import subprocess
9
9
  import shutil
10
- from InquirerPy import inquirer
11
10
  from urllib.parse import urlparse
12
11
  from typing import List
13
12
  from sima_cli.utils.env import get_environment_type
@@ -82,6 +81,11 @@ def _confirm_flavor_switching(full_image: bool, flavor: str) -> str:
82
81
  Returns:
83
82
  str: The flavor to use ('full' or 'headless')
84
83
  """
84
+ # when the flavor is set to auto use the detected flavor instead
85
+ if flavor == 'auto':
86
+ flavor = 'full' if full_image else 'headless'
87
+ click.echo(f"✅ Automatically detected the flavor of the running image: [{flavor}], Proceeding to update")
88
+
85
89
  if (full_image and flavor != 'full') or (not full_image and flavor == 'full'):
86
90
  click.echo(f"🔄 The current image running on the board has a different flavor from what you specified ({flavor}).")
87
91
  click.echo("Please choose an option:")
@@ -113,7 +117,9 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
113
117
  try:
114
118
  if len(available_versions) > 1:
115
119
  click.echo("Multiple firmware versions found matching your input:")
116
-
120
+
121
+ from InquirerPy import inquirer
122
+
117
123
  selected_version = inquirer.fuzzy(
118
124
  message="Select a version:",
119
125
  choices=available_versions,
@@ -440,7 +446,7 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
440
446
 
441
447
  # Get remote board info
442
448
  click.echo("🔍 Checking remote board type and version...")
443
- remote_board, remote_version, fdt_name, full_image = get_remote_board_info(ip, passwd)
449
+ remote_board, remote_version, _, _ = get_remote_board_info(ip, passwd)
444
450
 
445
451
  if not remote_board:
446
452
  click.echo("❌ Could not determine remote board type.")
@@ -480,7 +486,7 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
480
486
  extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor, swtype=swtype)
481
487
  return extracted_paths
482
488
 
483
- def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'headless'):
489
+ def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'auto'):
484
490
  r"""
485
491
  Update the system based on environment and input.
486
492
 
@@ -495,7 +501,7 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
495
501
  internal (bool): If True, enable internal-only behaviors (e.g., Artifactory access).
496
502
  passwd (str): Password for the board user (default: "edgeai").
497
503
  auto_confirm (bool): If True, auto-confirm firmware update without prompting.
498
- flavor (str): headless or full
504
+ flavor (str): headless or full, or auto (detect the running image flavor and use it)
499
505
  """
500
506
  try:
501
507
  board = ''
@@ -504,17 +510,17 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
504
510
  click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
505
511
 
506
512
  if env_type == 'board':
507
- board, version, fdt_name, full_image = get_local_board_info()
513
+ board, version, devkit_name, full_image = get_local_board_info()
508
514
  else:
509
- board, version, fdt_name, full_image = get_remote_board_info(ip, passwd)
515
+ board, version, devkit_name, full_image = get_remote_board_info(ip, passwd)
510
516
 
511
517
  flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
512
518
 
513
519
  if board in ['davinci', 'modalix']:
514
- click.echo(f"🔧 Target board: {board} : {fdt_name}, board currently running: {version}, full_image: {full_image}")
520
+ click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}")
515
521
 
516
- if flavor == 'full' and 'modalix' not in fdt_name:
517
- click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
522
+ if flavor == 'full' and 'modalix' not in devkit_name:
523
+ click.echo(f"❌ You've requested updating {devkit_name} to full image, this is only supported for the Modalix DevKit")
518
524
  return
519
525
 
520
526
  # Davinci only supports headless build, so ignore the full flavor
sima_cli/utils/env.py CHANGED
@@ -73,44 +73,51 @@ def get_sima_board_type() -> str:
73
73
 
74
74
  def is_modalix_devkit() -> bool:
75
75
  """
76
- Determines whether the current system is a Modalix DevKit (SOM)
77
- by checking if 'fdt_name=modalix-som.dtb' is present in fw_printenv output.
76
+ Determines whether the current system is a Modalix DevKit (SoM)
77
+ by checking if "Modalix SoM" is present in /proc/device-tree/model.
78
78
 
79
79
  Returns:
80
- bool: True if running on a Modalix DevKit, False otherwise.
80
+ bool: True if running on a Modalix DevKit (SoM), False otherwise.
81
81
  """
82
- if not shutil.which("fw_printenv"):
82
+ model_path = "/proc/device-tree/model"
83
+ if not os.path.exists(model_path):
83
84
  return False
84
85
 
85
86
  try:
86
- output = subprocess.check_output(["fw_printenv"], text=True)
87
- for line in output.splitlines():
88
- if line.strip().startswith("fdt_name="):
89
- return "modalix-som.dtb" in line
90
- except subprocess.CalledProcessError:
87
+ with open(model_path, "r") as f:
88
+ line = f.readline().strip()
89
+ return "Modalix SoM" in line
90
+ except Exception:
91
91
  return False
92
-
93
- return False
92
+
94
93
 
95
94
  def get_exact_devkit_type() -> str:
96
95
  """
97
- Extracts the exact devkit type from 'fdt_name' in fw_printenv output.
96
+ Extracts the exact devkit type from /proc/device-tree/model.
97
+
98
+ Example mappings:
99
+ "SiMa.ai Modalix SoM Board" -> "modalix-som"
100
+ "SiMa.ai Modalix DVT Board" -> "modalix-dvt"
101
+ "SiMa.ai DaVinci Half-Height..." -> "davinci-half-height-half-length"
98
102
 
99
103
  Returns:
100
- str: The value of fdt_name (e.g., "modalix-som"), or an empty string if not found or unavailable.
104
+ str: Normalized devkit type (lowercase, spaces -> "-"),
105
+ or an empty string if not found or unavailable.
101
106
  """
102
- if not shutil.which("fw_printenv"):
107
+ model_path = "/proc/device-tree/model"
108
+ if not os.path.exists(model_path):
103
109
  return ""
104
110
 
105
111
  try:
106
- output = subprocess.check_output(["fw_printenv"], text=True)
107
- for line in output.splitlines():
108
- line = line.strip()
109
- if line.startswith("fdt_name="):
110
- _, value = line.split("=", 1)
111
- return value.strip().replace('.dtb','')
112
- except subprocess.CalledProcessError:
113
- return ""
112
+ with open(model_path, "r") as f:
113
+ line = f.readline().strip()
114
+ # Remove "SiMa.ai " prefix if present
115
+ if line.startswith("SiMa.ai "):
116
+ line = line[len("SiMa.ai "):]
117
+ # Remove trailing "Board"
118
+ line = line.replace(" Board", "")
119
+ # Normalize
120
+ return line.lower().replace(" ", "-")
114
121
  except Exception:
115
122
  return ""
116
123
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.34
3
+ Version: 0.0.36
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,27 +1,27 @@
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=LhxWChlQ8xoXdQiVEuQK-O-_Z5UrnjYg4N-UwvKNSHk,49
4
- sima_cli/cli.py,sha256=uCZ-9mKBGyGtfx3p97WZkdMjXL2KJlqihfZm8VsWiqw,17220
3
+ sima_cli/__version__.py,sha256=Bbhf5gpY7WnVH4M-cc4ByQEC0_SMh5CebOd37T5sqCA,49
4
+ sima_cli/cli.py,sha256=qm8rtydcugVdd37Rd79weMdX78Y7SKWaUe4LGfX-1XY,17339
5
5
  sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sima_cli/auth/basic_auth.py,sha256=ht_mVXBtxV2UGvUYwvhkPHs4cMWL5Hw2B_OFxWdKw6c,8825
9
- sima_cli/auth/login.py,sha256=yCYXWgrfbP4jSTZ3hITfxlgHkdVQVzsd8hQKpqaqCKs,3780
8
+ sima_cli/auth/basic_auth.py,sha256=UMEXCCnNQpjpp6RZxREs6iiKtYyaeqZBnTSR0wA8s6Q,8767
9
+ sima_cli/auth/login.py,sha256=nE-dSHK_husXw1XJaEcOe3I1_bnwHkLgO_BqKuQODDM,3781
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
13
- sima_cli/download/downloader.py,sha256=nCBrr_0WdnKTIyecwKpg1sCdfm_4PSQTRPwEbiezy8M,5339
13
+ sima_cli/download/downloader.py,sha256=UQdrBWLQsPQygaoVaufOjbzWmRoNnk6pgLdnbnVi04U,6436
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=sVvMaUABra1FMgeTKK7Wj0u9UMamJ3wvJkSe_tuF0ug,19089
17
+ sima_cli/install/metadata_installer.py,sha256=zIxs9nSX7EC1d6qxL2woZiMSQfFfIOzB2natyTvIPDI,27428
18
18
  sima_cli/install/metadata_validator.py,sha256=7954rp9vFRNnqmIMvCVTjq40kUIEbGXzfc8HmQmChe0,5221
19
19
  sima_cli/install/optiview.py,sha256=r4DYdQDTUbZVCR87hl5T21gsjZrhqpU8hWnYxKmUJ_k,4790
20
20
  sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3930
21
21
  sima_cli/mla/meminfo.py,sha256=ndc8kQJmWGEIdvNh6iIhATGdrkqM2pbddr_eHxaPNfg,1466
22
22
  sima_cli/model_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  sima_cli/model_zoo/model.py,sha256=q91Nrg62j1TqwPO8HiX4nlEFCCmzNEFcyFTBVMbJm8w,9836
24
- sima_cli/network/network.py,sha256=ToDCQBfX0bUFEWWtfS8srImK5T11MX6R4MBQFM80faY,7617
24
+ sima_cli/network/network.py,sha256=kXYI2oxgeIg7LoGt2VKF9JlK8DIfQT87A9-x-D6uHCA,7714
25
25
  sima_cli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  sima_cli/sdk/syscheck.py,sha256=h9zCULW67y4i2hqiGc-hc1ucBDShA5FAe9NxwBGq-fM,4575
27
27
  sima_cli/serial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -32,21 +32,21 @@ 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=yOMvOu9nrODEzYZBrxUpdmlfqmkahkDk9nAEuG4RyAg,5588
35
+ sima_cli/update/local.py,sha256=z3oRk6JH-zbCdoS3JpgeW_ZB4kolG7nPhLC55A2yssk,5597
36
36
  sima_cli/update/netboot.py,sha256=hsJQLq4HVwFFkaWjA54VZdkMGDhO0RmylciS78qAfrM,19663
37
37
  sima_cli/update/query.py,sha256=6RgvQfQT1_EtBGcibvVcz003dRKOq17NaGgL2mhaBbY,4891
38
- sima_cli/update/remote.py,sha256=dAMIGpHqpf7VBps9JPe4hfHD_qyi1tG6ZW8E_qXQQiQ,14446
39
- sima_cli/update/updater.py,sha256=UEWcV3wZbikNaTMk1nWFGiciBwUZtdX94reZWn22plk,24213
38
+ sima_cli/update/remote.py,sha256=wC4MSBQVxmibxtPBchAzFMhZYcRjxTiLlPSzVI0en4o,14690
39
+ sima_cli/update/updater.py,sha256=NPd32OzM_356Nm1bEDDNh7faubW_VSOk12J4I6Yc2SQ,24580
40
40
  sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
42
42
  sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
43
43
  sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
44
44
  sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
45
- sima_cli/utils/env.py,sha256=IP5HrH0lE7RMSiBeXcEt5GCLMT5p-QQroG-uGzl5XFU,8181
45
+ sima_cli/utils/env.py,sha256=LtV8S1kCkOpi-7Gj4rhidQRN13x_NDKy4W_LxujheeI,8400
46
46
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
47
47
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
48
48
  sima_cli/utils/pkg_update_check.py,sha256=IAV_NAOsBDL_lYNYMRYfdZWuVq-rJ_zzHjJJZ7UQaoc,3274
49
- sima_cli-0.0.34.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
49
+ sima_cli-0.0.36.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
50
50
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -55,8 +55,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
55
55
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- sima_cli-0.0.34.dist-info/METADATA,sha256=h10I6DLYXz3ryXHSNaSW5Ie3pWKkhf6bwCBgXyQiDSE,3705
59
- sima_cli-0.0.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
- sima_cli-0.0.34.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
61
- sima_cli-0.0.34.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
62
- sima_cli-0.0.34.dist-info/RECORD,,
58
+ sima_cli-0.0.36.dist-info/METADATA,sha256=nRB0ysTR2pmyDNlBC6Tx7gk1VPJ6o83ZulKr99RLJTo,3705
59
+ sima_cli-0.0.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
+ sima_cli-0.0.36.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
61
+ sima_cli-0.0.36.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
62
+ sima_cli-0.0.36.dist-info/RECORD,,