sima-cli 0.0.30__py3-none-any.whl → 0.0.32__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.30"
2
+ __version__ = "0.0.32"
@@ -3,7 +3,10 @@ import click
3
3
  import getpass
4
4
  import requests
5
5
  import json
6
+ import tempfile
7
+
6
8
  from http.cookiejar import MozillaCookieJar
9
+ from sima_cli.__version__ import __version__
7
10
 
8
11
  HOME_DIR = os.path.expanduser("~/.sima-cli")
9
12
  COOKIE_JAR_PATH = os.path.join(HOME_DIR, ".sima-cli-cookies.txt")
@@ -11,10 +14,10 @@ CSRF_PATH = os.path.join(HOME_DIR, ".sima-cli-csrf.json")
11
14
 
12
15
  CSRF_URL = "https://developer.sima.ai/session/csrf"
13
16
  LOGIN_URL = "https://developer.sima.ai/session"
14
- DUMMY_CHECK_URL = "https://docs.sima.ai/pkg_downloads/dummy"
17
+ DUMMY_CHECK_URL = "https://docs.sima.ai/pkg_downloads/validation"
15
18
 
16
19
  HEADERS = {
17
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
20
+ "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",
18
21
  "X-Requested-With": "XMLHttpRequest",
19
22
  "Referer": "https://developer.sima.ai/login",
20
23
  "Origin": "https://developer.sima.ai",
@@ -24,12 +27,36 @@ HEADERS = {
24
27
  "sec-ch-ua-platform": '"macOS"',
25
28
  }
26
29
 
30
+ def _handle_eula_flow(session: requests.Session, username: str, domain: str) -> bool:
31
+ try:
32
+ click.echo("\n📄 To continue, you must accept the End-User License Agreement (EULA).")
33
+ click.echo("👉 Please sign in to Developer Portal on your browser, then open the following URL to accept the EULA:")
34
+ click.echo(f"\n {DUMMY_CHECK_URL}\n")
35
+
36
+ if not click.confirm("✅ Have you completed the EULA form in your browser?", default=True):
37
+ click.echo("❌ EULA acceptance is required to continue.")
38
+ return False
39
+
40
+ return True
41
+
42
+ except Exception as e:
43
+ click.echo(f"❌ Error during EULA flow: {e}")
44
+ return False
27
45
 
28
46
  def _is_session_valid(session: requests.Session) -> bool:
29
47
  try:
30
48
  response = session.get(DUMMY_CHECK_URL, allow_redirects=False)
31
- return response.status_code == 200
32
- except Exception:
49
+
50
+ if response.status_code == 200:
51
+ return True
52
+ elif response.status_code == 302:
53
+ location = response.headers.get("Location", "")
54
+ if "show-eula-form=1" in location:
55
+ return _handle_eula_flow(session, username="", domain="")
56
+
57
+ return False
58
+ except Exception as e:
59
+ click.echo(f"❌ Error validating session: {e}")
33
60
  return False
34
61
 
35
62
  def _delete_auth_files():
sima_cli/cli.py CHANGED
@@ -14,6 +14,7 @@ from sima_cli.serial.serial import connect_serial
14
14
  from sima_cli.storage.nvme import nvme_format, nvme_remount
15
15
  from sima_cli.storage.sdcard import sdcard_format
16
16
  from sima_cli.network.network import network_menu
17
+ from sima_cli.utils.pkg_update_check import check_for_update
17
18
 
18
19
  # Entry point for the CLI tool using Click's command group decorator
19
20
  @click.group()
@@ -26,6 +27,7 @@ def main(ctx, internal):
26
27
  Global Options:
27
28
  --internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
28
29
  """
30
+ check_for_update('sima-cli')
29
31
  ctx.ensure_object(dict)
30
32
 
31
33
  os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
@@ -6,19 +6,65 @@ import tarfile
6
6
  import os
7
7
  import subprocess
8
8
  from pathlib import Path
9
+ import tempfile
10
+ import stat
9
11
  from sima_cli.download import download_file_from_url
10
12
  from sima_cli.utils.env import is_sima_board
11
13
  from sima_cli.utils.config_loader import load_resource_config
12
14
 
15
+
16
+ def install_optiview_devkit():
17
+ """
18
+ Install optiview under /data/optiview with system site packages visible,
19
+ and add an alias to call it via sudo from .bashrc.
20
+ """
21
+ optiview_dir = "/data/optiview"
22
+ venv_dir = f"{optiview_dir}/.venv"
23
+
24
+ if is_sima_board():
25
+ click.echo("🛠 Detected SiMa DevKit. Cleaning up existing installation...")
26
+
27
+ # Ensure base folder exists
28
+ subprocess.run(["sudo", "mkdir", "-p", optiview_dir], check=True)
29
+
30
+ # Remove any old installation inside venv
31
+ subprocess.run(["sudo", "pip3", "uninstall", "-y", "optiview"], check=False)
32
+
33
+ click.echo("📦 Creating virtual environment with system site packages...")
34
+ subprocess.run([
35
+ "sudo", "python3", "-m", "venv", "--system-site-packages", venv_dir
36
+ ], check=True)
37
+
38
+ click.echo("📦 Installing Optiview via pip inside venv...")
39
+ subprocess.run([
40
+ "sudo", f"{venv_dir}/bin/pip", "install", "optiview"
41
+ ], check=True)
42
+
43
+ # Add alias to ~/.bashrc for sudo launch
44
+ alias_cmd = f"alias optiview='sudo {venv_dir}/bin/optiview'"
45
+ bashrc_path = os.path.expanduser("~/.bashrc")
46
+
47
+ # Ensure idempotent append
48
+ with open(bashrc_path, "a+") as f:
49
+ f.seek(0)
50
+ if alias_cmd not in f.read():
51
+ f.write(f"\n# Optiview alias\n{alias_cmd}\n")
52
+ click.echo(f"🔗 Added alias to {bashrc_path}")
53
+ else:
54
+ click.echo(f"ℹ️ Alias already exists in {bashrc_path}")
55
+
56
+ click.echo("✅ Optiview installed successfully on DevKit.")
57
+ click.echo("ℹ️ Restart your shell or run 'source ~/.bashrc' to use the alias.")
58
+
59
+ return True
60
+
61
+ return False
62
+
13
63
  def install_optiview():
14
64
  try:
15
65
  # Special path for SiMa DevKit
16
66
  if is_sima_board():
17
- click.echo("🛠 Detected SiMa DevKit. Cleaning up existing installation...")
18
- subprocess.run(["sudo", "pip3", "uninstall", "-y", "optiview"], check=True)
19
- click.echo("📦 Installing Optiview via pip...")
20
- subprocess.run(["sudo", "pip3", "install", "optiview"], check=True)
21
- click.echo("✅ Optiview installed successfully on DevKit.")
67
+ install_optiview_devkit()
22
68
  return
23
69
 
24
70
  cfg = load_resource_config()
@@ -0,0 +1,26 @@
1
+ import re
2
+ from typing import Iterable, Pattern, List
3
+
4
+ # Common noisy bits you mentioned; add your own here.
5
+ DEFAULT_NOISE_PATTERNS = [
6
+ r"Information:\s+You may need to update /etc/fstab\.", # fdisk/parted hint
7
+ r"resize2fs\s+\d+\.\d+\.\d+\s*\([^)]+\)", # e2fsprogs banner
8
+ # add more as needed...
9
+ ]
10
+
11
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
12
+
13
+ def strip_ansi(s: str) -> str:
14
+ return _ANSI_RE.sub("", s)
15
+
16
+ class LineSquelcher:
17
+ """Return False for lines we want to hide."""
18
+ def __init__(self,
19
+ patterns: Iterable[str] = DEFAULT_NOISE_PATTERNS,
20
+ ignore_case: bool = True):
21
+ flags = re.IGNORECASE if ignore_case else 0
22
+ self._rx: List[Pattern[str]] = [re.compile(p, flags) for p in patterns]
23
+
24
+ def allow(self, line: str) -> bool:
25
+ clean = strip_ansi(line)
26
+ return not any(rx.search(clean) for rx in self._rx)
sima_cli/update/local.py CHANGED
@@ -2,13 +2,22 @@ import os
2
2
  from typing import Tuple
3
3
  import pty
4
4
  import click
5
+ import re
5
6
 
7
+ from typing import Optional
6
8
  from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type
9
+ from sima_cli.update.cleanlog import LineSquelcher
7
10
 
8
- def _run_local_cmd(command: str, passwd: str) -> bool:
11
+
12
+ def _run_local_cmd(
13
+ command: str,
14
+ passwd: str,
15
+ squelcher: Optional[LineSquelcher] = None,
16
+ show_summary: bool = True,
17
+ ) -> bool:
9
18
  """
10
- Run a local command using a pseudo-terminal (pty) to force live output flushing,
11
- and optionally pass a sudo password.
19
+ Run a local command using a PTY for live output.
20
+ Filters out noisy lines using LineSquelcher (if provided or default).
12
21
  """
13
22
  click.echo(f"🖥️ Running: {command}")
14
23
 
@@ -16,6 +25,10 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
16
25
  if needs_sudo:
17
26
  command = f"sudo -S {command[len('sudo '):]}"
18
27
 
28
+ squelcher = squelcher or LineSquelcher()
29
+ suppressed = 0
30
+ buf = "" # carry partial lines between reads
31
+
19
32
  try:
20
33
  pid, fd = pty.fork()
21
34
 
@@ -24,19 +37,43 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
24
37
  os.execvp("sh", ["sh", "-c", command])
25
38
  else:
26
39
  if needs_sudo:
40
+ # Send the sudo password immediately (stdin is the PTY)
27
41
  os.write(fd, (passwd + "\n").encode())
28
42
 
29
43
  while True:
30
44
  try:
31
- output = os.read(fd, 1024).decode()
32
- if not output:
45
+ chunk = os.read(fd, 4096)
46
+ if not chunk:
33
47
  break
34
- for line in output.splitlines():
35
- click.echo(line)
48
+
49
+ # Decode & normalize progress lines: turn lone \r into \n
50
+ text = chunk.decode("utf-8", errors="replace")
51
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
52
+
53
+ buf += text
54
+ *lines, buf = buf.split("\n")
55
+
56
+ for line in lines:
57
+ if squelcher.allow(line):
58
+ click.echo(line)
59
+ else:
60
+ suppressed += 1
61
+
36
62
  except OSError:
37
63
  break
38
64
 
65
+ # Flush any remaining partial line
66
+ if buf:
67
+ if squelcher.allow(buf):
68
+ click.echo(buf)
69
+ else:
70
+ suppressed += 1
71
+
39
72
  _, status = os.waitpid(pid, 0)
73
+
74
+ if show_summary and suppressed:
75
+ click.echo(f"🔇 suppressed {suppressed} noisy line(s)")
76
+
40
77
  return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
41
78
 
42
79
  except Exception as e:
@@ -75,6 +112,31 @@ def get_local_board_info() -> Tuple[str, str, bool]:
75
112
  return board_type, build_version, fdt_name, is_board_running_full_image()
76
113
 
77
114
 
115
+ def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
116
+ """
117
+ Figure out which eMMC the device was booted from - local version
118
+ """
119
+ try:
120
+ with open(mounts_path) as f:
121
+ for line in f:
122
+ dev, mnt = line.split()[:2]
123
+ if mnt == "/":
124
+ m = re.search(r'(mmcblk\d+)', dev)
125
+ if m:
126
+ return m.group(1)
127
+ except OSError:
128
+ pass
129
+
130
+ try:
131
+ with open(cmdline_path) as f:
132
+ m = re.search(r'\broot=(?:/dev/)?(mmcblk\d+)', f.read())
133
+ if m:
134
+ return m.group(1)
135
+ except OSError:
136
+ pass
137
+
138
+ return None
139
+
78
140
  def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str, flavor: str):
79
141
  """
80
142
  Perform local firmware update using swupdate commands.
@@ -83,6 +145,11 @@ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str,
83
145
  click.echo("📦 Starting local firmware update...")
84
146
 
85
147
  try:
148
+ blk = get_boot_mmc()
149
+ if blk != None:
150
+ fix_gpt_cmd = f'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/{blk} print'
151
+ _run_local_cmd(fix_gpt_cmd, passwd)
152
+
86
153
  # Run tRoot update
87
154
  if troot_path != None:
88
155
  click.echo("⚙️ Flashing tRoot image...")
sima_cli/update/query.py CHANGED
@@ -39,6 +39,10 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
39
39
  }
40
40
 
41
41
  response = requests.post(aql_url, data=aql_query, headers=headers)
42
+
43
+ if response.status_code == 401:
44
+ print('❌ You are not authorized to access Artifactory, use `sima-cli -i login` with your Artifactory identity token to authenticate, then try the command again.')
45
+
42
46
  if response.status_code != 200:
43
47
  return None
44
48
 
sima_cli/update/remote.py CHANGED
@@ -7,7 +7,9 @@ import itertools
7
7
  import threading
8
8
  import sys
9
9
  import select
10
- from typing import Tuple
10
+ from typing import Tuple, Optional
11
+
12
+ from sima_cli.update.cleanlog import LineSquelcher
11
13
 
12
14
  DEFAULT_USER = "sima"
13
15
  DEFAULT_PASSWORD = "edgeai"
@@ -97,7 +99,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
97
99
 
98
100
  for line in fdt_output.splitlines():
99
101
  if line.startswith("fdt_name"):
100
- fdt_name = line.split("=", 1)[-1].strip()
102
+ fdt_name = line.split("=", 1)[-1].strip().replace('.dtb', '')
101
103
 
102
104
  return board_type, build_version, fdt_name, full_image
103
105
 
@@ -113,7 +115,9 @@ def _scp_file(sftp, local_path: str, remote_path: str):
113
115
  sftp.put(local_path, remote_path)
114
116
  click.echo("✅ Upload complete")
115
117
 
116
- def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
118
+
119
+ def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD,
120
+ squelcher: Optional[LineSquelcher] = None):
117
121
  """
118
122
  Run a remote command over SSH and stream its output live to the console.
119
123
  If the command starts with 'sudo', pipe in the password.
@@ -123,40 +127,56 @@ def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
123
127
  command (str): The command to run on the remote host.
124
128
  password (str): Password to use if the command requires sudo.
125
129
  """
126
- click.echo(f"🚀 Running on remote: {command}")
130
+ squelcher = squelcher or LineSquelcher() # use defaults unless you pass a custom one
127
131
 
132
+ click.echo(f"🚀 Running on remote: {command}")
128
133
  needs_sudo = command.strip().startswith("sudo")
129
134
  if needs_sudo:
130
- # Use -S to allow password from stdin
131
135
  command = f"sudo -S {command[len('sudo '):]}"
132
136
 
133
137
  stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
134
-
135
138
  if needs_sudo:
136
- # Send password immediately, followed by newline
137
139
  stdin.write(password + "\n")
138
140
  stdin.flush()
139
141
 
142
+ suppressed = 0
140
143
  while not stdout.channel.exit_status_ready():
141
144
  rl, _, _ = select.select([stdout.channel], [], [], 0.5)
142
145
  if rl:
143
146
  if stdout.channel.recv_ready():
144
147
  output = stdout.channel.recv(4096).decode("utf-8", errors="replace")
145
148
  for line in output.splitlines():
146
- click.echo(f"↦ {line}")
149
+ if squelcher.allow(line):
150
+ click.echo(f"↦ {line}")
151
+ else:
152
+ suppressed += 1
147
153
  if stdout.channel.recv_stderr_ready():
148
154
  err_output = stdout.channel.recv_stderr(4096).decode("utf-8", errors="replace")
149
155
  for line in err_output.splitlines():
150
- click.echo(f"⚠️ {line}")
156
+ if squelcher.allow(line):
157
+ click.echo(f"⚠️ {line}")
158
+ else:
159
+ suppressed += 1
151
160
 
152
161
  # Final remaining output
153
162
  remaining = stdout.read().decode("utf-8", errors="replace")
154
163
  for line in remaining.splitlines():
155
- click.echo(f"↦ {line}")
164
+ if squelcher.allow(line):
165
+ click.echo(f"↦ {line}")
166
+ else:
167
+ suppressed += 1
156
168
 
157
169
  remaining_err = stderr.read().decode("utf-8", errors="replace")
158
170
  for line in remaining_err.splitlines():
159
- click.echo(f"⚠️ {line}")
171
+ if squelcher.allow(line):
172
+ click.echo(f"⚠️ {line}")
173
+ else:
174
+ suppressed += 1
175
+
176
+ # Optional: surface how much noise we hid (comment out if you want it totally silent)
177
+ if suppressed:
178
+ click.echo(f"🔇 suppressed {suppressed} noisy line(s)")
179
+
160
180
 
161
181
  def init_ssh_session(ip: str, password: str = DEFAULT_PASSWORD):
162
182
  ssh = paramiko.SSHClient()
@@ -182,6 +202,54 @@ def reboot_remote_board(ip: str, passwd: str):
182
202
  click.echo(f"⚠️ Unable to connect to the remote board")
183
203
 
184
204
 
205
+ def run_remote_command_capture(ssh, command: str, password: str = DEFAULT_PASSWORD):
206
+ """
207
+ Run a remote command over SSH and return (exit_status, stdout_str, stderr_str).
208
+ Does not stream output to the console.
209
+ If the command starts with 'sudo', it will send the password via stdin.
210
+ """
211
+ needs_sudo = command.strip().startswith("sudo")
212
+ if needs_sudo:
213
+ command = f"sudo -S {command[len('sudo '):]}"
214
+
215
+ stdin, stdout, stderr = ssh.exec_command(command, get_pty=needs_sudo)
216
+ if needs_sudo:
217
+ stdin.write(password + "\n")
218
+ stdin.flush()
219
+
220
+ out = stdout.read().decode("utf-8", errors="replace")
221
+ err = stderr.read().decode("utf-8", errors="replace")
222
+ code = stdout.channel.recv_exit_status()
223
+ return code, out, err
224
+
225
+
226
+ def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> str | None:
227
+ """
228
+ Determine the remote boot device: 'mmcblk0', 'mmcblk1', or None.
229
+
230
+ Strategy (remote):
231
+ 1) Look at the actual device mounted as '/' in /proc/mounts.
232
+ 2) Fallback to parsing /proc/cmdline (root=...).
233
+ """
234
+ # Minimal, BusyBox-friendly shell: grep/awk only, no fancy sed EREs.
235
+ remote_script = r"""
236
+ mmc="$(awk '$2=="/"{print $1}' /proc/mounts | grep -oE 'mmcblk[0-9]+' | head -n1)"
237
+ if [ -z "$mmc" ]; then
238
+ root_tok="$(sed -n 's/.*root=\([^ ]*\).*/\1/p' /proc/cmdline)"
239
+ mmc="$(printf '%s\n' "$root_tok" | grep -oE 'mmcblk[0-9]+' | head -n1)"
240
+ fi
241
+ [ -n "$mmc" ] && printf '%s\n' "$mmc"
242
+ """
243
+
244
+ code, out, err = run_remote_command_capture(ssh, f"sh -c {quote_shell(remote_script)}", password=password)
245
+ mmc = out.strip()
246
+ return mmc if mmc else None
247
+
248
+ def quote_shell(s: str) -> str:
249
+ """Safely single-quote a string for sh -c."""
250
+ # Turn: abc'def -> 'abc'"'"'def'
251
+ return "'" + s.replace("'", "'\"'\"'") + "'"
252
+
185
253
  def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd: str):
186
254
  """
187
255
  Copy a file to the remote board over SSH.
@@ -220,6 +288,12 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
220
288
  remote_dir = "/tmp"
221
289
  palette_name = os.path.basename(palette_path)
222
290
 
291
+ boot_mmc = get_remote_boot_mmc(ssh, passwd)
292
+ if boot_mmc != None:
293
+ click.echo(f'✅ Checking partition table GPT record for {boot_mmc}...')
294
+ fix_gpt_cmd = f'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/{boot_mmc} print'
295
+ run_remote_command(ssh, fix_gpt_cmd)
296
+
223
297
  # Upload tRoot image
224
298
  if troot_path is not None:
225
299
  troot_name = os.path.basename(troot_path)
@@ -231,9 +305,10 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
231
305
  ssh,
232
306
  f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
233
307
  )
234
- click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
235
- click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
236
- _wait_for_ssh(ip, timeout=120)
308
+ # Disabled the following code per agreement with QA, this reboot step is not required for tRoot update
309
+ # click.echo(" tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
310
+ # click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
311
+ # _wait_for_ssh(ip, timeout=120)
237
312
  else:
238
313
  click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
239
314
 
@@ -137,7 +137,7 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
137
137
  )
138
138
  raise SystemExit(1)
139
139
  except:
140
- click.echo("Unable to determine available versions")
140
+ click.echo("Unable to determine available versions")
141
141
  exit(0)
142
142
 
143
143
  def _sanitize_url_to_filename(url: str) -> str:
@@ -511,9 +511,9 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
511
511
  flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
512
512
 
513
513
  if board in ['davinci', 'modalix']:
514
- click.echo(f"🔧 Target board: {board} {fdt_name}, board currently running: {version}, full_image: {full_image}")
514
+ click.echo(f"🔧 Target board: {board} : {fdt_name}, board currently running: {version}, full_image: {full_image}")
515
515
 
516
- if flavor == 'full' and fdt_name != 'modalix-som.dtb':
516
+ if flavor == 'full' and fdt_name != 'modalix-som':
517
517
  click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
518
518
  return
519
519
 
@@ -539,9 +539,13 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
539
539
  if env_type == "host" and env_subtype == 'linux':
540
540
  # Always update the remote device first then update the host driver, otherwise the host would
541
541
  # not be able to connect to the board
542
- click.echo("👉 Updating PCIe host driver and downloading firmware...")
543
542
  script_path = _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=False, flavor=flavor)
544
- _update_host(script_path, board, ip, passwd)
543
+ click.echo("👉 sima-cli detected you are updating the board on a Linux host...")
544
+ if click.confirm("👉 Do you want to update the host PCIe driver now? If you do not intend to use a Sima PCIe card ever on this machine, enter N", default=False):
545
+ click.echo("👉 Updating PCIe host driver and downloading firmware...")
546
+ _update_host(script_path, board, ip, passwd)
547
+ else:
548
+ click.echo("⚠️ Skipping Linux host driver update.")
545
549
  elif env_type == "board":
546
550
  _update_board(extracted_paths, board, passwd, flavor=flavor)
547
551
  elif env_type == "sdk":
@@ -0,0 +1,84 @@
1
+ import importlib.metadata
2
+ import urllib.request
3
+ import subprocess
4
+ import json
5
+ import socket
6
+ import click
7
+ import sys
8
+ import shlex
9
+ import shutil
10
+ import glob
11
+ import os
12
+
13
+ def cleanup_pip_leftovers():
14
+ """Remove ~-prefixed leftover dirs in site-packages."""
15
+ for path in sys.path:
16
+ if path.endswith("site-packages") and os.path.isdir(path):
17
+ junk_dirs = glob.glob(os.path.join(path, "~*"))
18
+ for d in junk_dirs:
19
+ try:
20
+ shutil.rmtree(d, ignore_errors=True)
21
+ except Exception as e:
22
+ click.secho(f"⚠️ Failed to remove {d}: {e}", fg="yellow")
23
+
24
+ def update_package(package_name: str):
25
+ """Suggest manual update on Windows; auto-update elsewhere."""
26
+ pip_cmd = f"{shlex.quote(sys.executable)} -m pip install --upgrade {package_name}"
27
+
28
+ if sys.platform.startswith("win"):
29
+ click.secho("⚠️ Automatic self-update is not supported on Windows while the CLI is running.", fg="yellow", bold=True)
30
+ safe_cmd = pip_cmd.replace("'", "")
31
+ click.echo(f"Please run the following command in a new terminal:\n\n {safe_cmd}\n")
32
+ return
33
+
34
+ try:
35
+ subprocess.run(shlex.split(pip_cmd), check=True)
36
+ cleanup_pip_leftovers()
37
+ click.secho(f"✅ {package_name} updated successfully.", fg="green", bold=True)
38
+ except subprocess.CalledProcessError as e:
39
+ click.secho(f"❌ Failed to update {package_name}: {e}", fg="red", bold=True)
40
+
41
+ def has_internet(timeout: float = 1.0) -> bool:
42
+ """
43
+ Quick check for internet connectivity by connecting to a known DNS server.
44
+ Uses IP to avoid DNS lookup delays.
45
+ """
46
+ try:
47
+ socket.setdefaulttimeout(timeout)
48
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
49
+ sock.connect(("1.1.1.1", 53))
50
+ sock.close()
51
+ return True
52
+ except OSError:
53
+ return False
54
+
55
+ def check_for_update(package_name: str, timeout: float = 2.0):
56
+
57
+ if os.environ.get("SIMA_CLI_CHECK_FOR_UPDATE", "1") != "1":
58
+ print(f'⚠️ You have disabled update check with SIMA_CLI_CHECK_FOR_UPDATE environment variable, skipping sima-cli update check..')
59
+ return
60
+
61
+ try:
62
+ current_version = importlib.metadata.version(package_name)
63
+ except importlib.metadata.PackageNotFoundError:
64
+ print(f'❌ package not found {package_name}')
65
+ return
66
+
67
+ if not has_internet(timeout=0.2):
68
+ print(f'⚠️ Offline mode, skipping sima-cli update check..')
69
+ return
70
+
71
+ try:
72
+ with urllib.request.urlopen(f"https://pypi.org/pypi/{package_name}/json", timeout=timeout) as resp:
73
+ latest_version = json.load(resp)["info"]["version"]
74
+ except Exception:
75
+ return # PyPI unreachable or network error; skip
76
+
77
+ if current_version != latest_version:
78
+ click.secho(f"🔔 Update available: {current_version} → {latest_version}", fg="green", bold=True)
79
+ click.secho(f"🔔 If you don't want to automatically check for updates, set SIMA_CLI_CHECK_FOR_UPDATE environment variable to 0")
80
+ if click.confirm(f"🔔 Do you want to update {package_name} now?", default=True):
81
+ update_package(package_name)
82
+ exit(0)
83
+ else:
84
+ print('✅ sima-cli is up-to-date')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.30
3
+ Version: 0.0.32
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,11 +1,11 @@
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=fcUZK1xhI360Deo68dmn4Df_ffnBdYiwwdvLTu09P58,49
4
- sima_cli/cli.py,sha256=GYmQ7_XObl9VgFwuWWkWDo-_Y_Vn6lM53F7mKiYGubI,17126
3
+ sima_cli/__version__.py,sha256=GQtUV16wGw39emS7dJUNBY68tz0z4U8p69F7t8QwZ24,49
4
+ sima_cli/cli.py,sha256=uCZ-9mKBGyGtfx3p97WZkdMjXL2KJlqihfZm8VsWiqw,17220
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=mEmPrj32TVu1s34xR_UrJlIKHA3xBh98i_FzIZvAWag,7364
8
+ sima_cli/auth/basic_auth.py,sha256=rJwxRoZSN5bt_v8-IFZHsRp4YHfxogFvU_M6wAp229k,8473
9
9
  sima_cli/auth/login.py,sha256=yCYXWgrfbP4jSTZ3hITfxlgHkdVQVzsd8hQKpqaqCKs,3780
10
10
  sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
11
11
  sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
@@ -16,7 +16,7 @@ sima_cli/install/hostdriver.py,sha256=kAWDLebs60mbWIyTbUxmNrChcKW1uD5r7FtWNSUVUE
16
16
  sima_cli/install/metadata_info.py,sha256=wmMqwzGfXbuilkqaxRVrFOzOtTOiONkmPCyA2oDAQpA,2168
17
17
  sima_cli/install/metadata_installer.py,sha256=UPXxXL5gH0iotX8WCUgEbySbYeIHE1UwsfpZvACjZQs,18928
18
18
  sima_cli/install/metadata_validator.py,sha256=7954rp9vFRNnqmIMvCVTjq40kUIEbGXzfc8HmQmChe0,5221
19
- sima_cli/install/optiview.py,sha256=i5eWVor-9MScEfrQm3Ty9OP4VpSsCgWvNh7AvYdZu7s,3365
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
@@ -31,11 +31,12 @@ sima_cli/storage/sdcard.py,sha256=-WULjdV31-n8v5OOqfxR77qBbIK4hJnrD3xWxUVMoGI,63
31
31
  sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
32
32
  sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
33
33
  sima_cli/update/bootimg.py,sha256=Eg8ZSp8LMZXbOMxX4ZPCjFOg3YEufmsVfojKrRc3fug,13631
34
- sima_cli/update/local.py,sha256=no3PDChERbBcyjeNVAMR4dH4OaMoRUv8hpym-aoFdhQ,3597
34
+ sima_cli/update/cleanlog.py,sha256=-V6eDl3MdsvDmCfkKUJTqkXJ_WnLJE01uxS7z96b15g,909
35
+ sima_cli/update/local.py,sha256=yOMvOu9nrODEzYZBrxUpdmlfqmkahkDk9nAEuG4RyAg,5588
35
36
  sima_cli/update/netboot.py,sha256=hsJQLq4HVwFFkaWjA54VZdkMGDhO0RmylciS78qAfrM,19663
36
- sima_cli/update/query.py,sha256=b6Su7OlBGooIDcpb3_xH55tqkzznWcd_Fg1MkaNR874,4680
37
- sima_cli/update/remote.py,sha256=ieZpKN2PCMu90Q72PnN66DZYyqpvBjQYHiDIjvdS5BY,11475
38
- sima_cli/update/updater.py,sha256=-NXrGbPaxsj69NjdG_1GTB6g2bZMzk8sHJEg4NaKSgQ,23807
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=Bi-9apDXJlnQg_sUQ46rQPXD6zbx4uE_JhXbfJIGBxE,24213
39
40
  sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
41
42
  sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
@@ -44,7 +45,8 @@ sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
44
45
  sima_cli/utils/env.py,sha256=IP5HrH0lE7RMSiBeXcEt5GCLMT5p-QQroG-uGzl5XFU,8181
45
46
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
46
47
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
47
- sima_cli-0.0.30.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
48
+ sima_cli/utils/pkg_update_check.py,sha256=IAV_NAOsBDL_lYNYMRYfdZWuVq-rJ_zzHjJJZ7UQaoc,3274
49
+ sima_cli-0.0.32.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
48
50
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
51
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
52
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -53,8 +55,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
53
55
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
56
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
57
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- sima_cli-0.0.30.dist-info/METADATA,sha256=CywF-g-sv44c9qK1UIAu3AROiPOwnPISqiSSQMnOEiY,3705
57
- sima_cli-0.0.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- sima_cli-0.0.30.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
59
- sima_cli-0.0.30.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
60
- sima_cli-0.0.30.dist-info/RECORD,,
58
+ sima_cli-0.0.32.dist-info/METADATA,sha256=U2WSNDVfQWPqZjPVGrSJKkSUeKHzY8cOs59vAgGq2UI,3705
59
+ sima_cli-0.0.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
+ sima_cli-0.0.32.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
61
+ sima_cli-0.0.32.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
62
+ sima_cli-0.0.32.dist-info/RECORD,,