replx 1.6__tar.gz → 1.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {replx-1.6/replx.egg-info → replx-1.6.1}/PKG-INFO +1 -1
  2. {replx-1.6 → replx-1.6.1}/replx/__init__.py +1 -1
  3. {replx-1.6 → replx-1.6.1}/replx/cli/agent/client/core.py +41 -8
  4. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/connection_manager.py +0 -5
  5. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/exec.py +27 -3
  6. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/session.py +0 -6
  7. {replx-1.6 → replx-1.6.1}/replx/cli/app.py +2 -2
  8. {replx-1.6 → replx-1.6.1}/replx/cli/commands/device.py +0 -35
  9. {replx-1.6 → replx-1.6.1}/replx/cli/commands/exec.py +205 -60
  10. {replx-1.6 → replx-1.6.1}/replx/cli/commands/file.py +16 -209
  11. {replx-1.6 → replx-1.6.1}/replx/cli/commands/firmware.py +3 -35
  12. {replx-1.6 → replx-1.6.1}/replx/cli/commands/package.py +1 -98
  13. {replx-1.6 → replx-1.6.1}/replx/cli/commands/utility.py +45 -98
  14. {replx-1.6 → replx-1.6.1}/replx/cli/config.py +0 -1
  15. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/compiler.py +0 -1
  16. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/output.py +1 -2
  17. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/updater.py +0 -1
  18. {replx-1.6 → replx-1.6.1}/replx/protocol/__init__.py +0 -1
  19. {replx-1.6 → replx-1.6.1}/replx/protocol/repl.py +7 -24
  20. {replx-1.6 → replx-1.6.1}/replx/protocol/storage.py +1 -45
  21. {replx-1.6 → replx-1.6.1}/replx/terminal.py +161 -7
  22. {replx-1.6 → replx-1.6.1/replx.egg-info}/PKG-INFO +1 -1
  23. {replx-1.6 → replx-1.6.1}/replx.egg-info/SOURCES.txt +1 -16
  24. replx-1.6.1/test/test_line_mode_terminal.py +428 -0
  25. replx-1.6.1/test/test_termio.py +53 -0
  26. replx-1.6/replx/tests/__init__.py +0 -0
  27. replx-1.6/replx/tests/test_agent_asyncio.py +0 -319
  28. replx-1.6/replx/tests/test_agent_port_canonicalization.py +0 -43
  29. replx-1.6/replx/tests/test_agent_thread_pool.py +0 -216
  30. replx-1.6/replx/tests/test_compiler_arch.py +0 -17
  31. replx-1.6/replx/tests/test_connection_info_lookup.py +0 -30
  32. replx-1.6/replx/tests/test_device_info_esp_multi_core.py +0 -56
  33. replx-1.6/replx/tests/test_disconnect_cleanup.py +0 -57
  34. replx-1.6/replx/tests/test_lock_cleanup.py +0 -194
  35. replx-1.6/replx/tests/test_pkg_local_version.py +0 -44
  36. replx-1.6/replx/tests/test_pkg_search_scope_filter.py +0 -82
  37. replx-1.6/replx/tests/test_repl_reader_task.py +0 -313
  38. replx-1.6/replx/tests/test_session_disconnect_releases_shared_port.py +0 -47
  39. replx-1.6/replx/tests/test_session_id_fallback.py +0 -32
  40. replx-1.6/replx/tests/test_shutdown_status_message.py +0 -56
  41. replx-1.6/replx/tests/test_windows_com_port_normalization.py +0 -69
  42. replx-1.6/test/test_termio.py +0 -70
  43. {replx-1.6 → replx-1.6.1}/LICENSE +0 -0
  44. {replx-1.6 → replx-1.6.1}/README.md +0 -0
  45. {replx-1.6 → replx-1.6.1}/pyproject.toml +0 -0
  46. {replx-1.6 → replx-1.6.1}/replx/cli/__init__.py +0 -0
  47. {replx-1.6 → replx-1.6.1}/replx/cli/agent/__init__.py +0 -0
  48. {replx-1.6 → replx-1.6.1}/replx/cli/agent/client/__init__.py +0 -0
  49. {replx-1.6 → replx-1.6.1}/replx/cli/agent/client/session.py +0 -0
  50. {replx-1.6 → replx-1.6.1}/replx/cli/agent/protocol.py +0 -0
  51. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/__init__.py +0 -0
  52. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/__main__.py +0 -0
  53. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/command_dispatcher.py +0 -0
  54. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/core.py +0 -0
  55. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/__init__.py +0 -0
  56. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/filesystem.py +0 -0
  57. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/repl.py +0 -0
  58. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/handlers/transfer.py +0 -0
  59. {replx-1.6 → replx-1.6.1}/replx/cli/agent/server/session_manager.py +0 -0
  60. {replx-1.6 → replx-1.6.1}/replx/cli/commands/__init__.py +0 -0
  61. {replx-1.6 → replx-1.6.1}/replx/cli/connection.py +0 -0
  62. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/__init__.py +0 -0
  63. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/environment.py +0 -0
  64. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/registry.py +0 -0
  65. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/scanner.py +0 -0
  66. {replx-1.6 → replx-1.6.1}/replx/cli/helpers/store.py +0 -0
  67. {replx-1.6 → replx-1.6.1}/replx/commands.py +0 -0
  68. {replx-1.6 → replx-1.6.1}/replx/transport/__init__.py +0 -0
  69. {replx-1.6 → replx-1.6.1}/replx/transport/base.py +0 -0
  70. {replx-1.6 → replx-1.6.1}/replx/transport/serial.py +0 -0
  71. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/_thread.pyi +0 -0
  72. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  73. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/array.pyi +0 -0
  74. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  75. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/binascii.pyi +0 -0
  76. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/bluetooth.pyi +0 -0
  77. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/builtins.pyi +0 -0
  78. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/cmath.pyi +0 -0
  79. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/collections.pyi +0 -0
  80. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/cryptolib.pyi +0 -0
  81. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/deflate.pyi +0 -0
  82. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/errno.pyi +0 -0
  83. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/framebuf.pyi +0 -0
  84. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/gc.pyi +0 -0
  85. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/hashlib.pyi +0 -0
  86. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/heapq.pyi +0 -0
  87. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/io.pyi +0 -0
  88. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/json.pyi +0 -0
  89. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/lwip.pyi +0 -0
  90. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/machine.pyi +0 -0
  91. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/math.pyi +0 -0
  92. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/micropython.pyi +0 -0
  93. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/mip/__init__.pyi +0 -0
  94. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/network.pyi +0 -0
  95. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/ntptime.pyi +0 -0
  96. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/os.pyi +0 -0
  97. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/platform.pyi +0 -0
  98. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/random.pyi +0 -0
  99. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/re.pyi +0 -0
  100. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/requests/__init__.pyi +0 -0
  101. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/select.pyi +0 -0
  102. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/socket.pyi +0 -0
  103. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/ssl.pyi +0 -0
  104. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/struct.pyi +0 -0
  105. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/sys.pyi +0 -0
  106. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/time.pyi +0 -0
  107. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/tls.pyi +0 -0
  108. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/uasyncio.pyi +0 -0
  109. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/uctypes.pyi +0 -0
  110. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/urequests.pyi +0 -0
  111. {replx-1.6 → replx-1.6.1}/replx/typehints/comm/vfs.pyi +0 -0
  112. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  113. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  114. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  115. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  116. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  117. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  118. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  119. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  120. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  121. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  122. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  123. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  124. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  125. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  126. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  127. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  128. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  129. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  130. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  131. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  132. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  133. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  134. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  135. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  136. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  137. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  138. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  139. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  140. {replx-1.6 → replx-1.6.1}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  141. {replx-1.6 → replx-1.6.1}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  142. {replx-1.6 → replx-1.6.1}/replx/typehints/core/ESP32/esp.pyi +0 -0
  143. {replx-1.6 → replx-1.6.1}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  144. {replx-1.6 → replx-1.6.1}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  145. {replx-1.6 → replx-1.6.1}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  146. {replx-1.6 → replx-1.6.1}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  147. {replx-1.6 → replx-1.6.1}/replx/utils/__init__.py +0 -0
  148. {replx-1.6 → replx-1.6.1}/replx/utils/constants.py +0 -0
  149. {replx-1.6 → replx-1.6.1}/replx/utils/device_info.py +0 -0
  150. {replx-1.6 → replx-1.6.1}/replx/utils/exceptions.py +0 -0
  151. {replx-1.6 → replx-1.6.1}/replx.egg-info/dependency_links.txt +0 -0
  152. {replx-1.6 → replx-1.6.1}/replx.egg-info/entry_points.txt +0 -0
  153. {replx-1.6 → replx-1.6.1}/replx.egg-info/requires.txt +0 -0
  154. {replx-1.6 → replx-1.6.1}/replx.egg-info/top_level.txt +0 -0
  155. {replx-1.6 → replx-1.6.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replx
3
- Version: 1.6
3
+ Version: 1.6.1
4
4
  Summary: replx is a fast, modern MicroPython CLI: turbo REPL, robust file sync (put/get), project install, mpy-cross integration, and smart port discovery.
5
5
  Author-email: "chanmin.park" <devcamp@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.6"
2
+ __version__ = "1.6.1"
3
3
  __description__ = "Fast, modern MicroPython CLI with REPL, file sync, install, and smart port detection."
4
4
  __author__ = "PlanX Lab Development Team"
5
5
 
@@ -2,6 +2,7 @@ import os
2
2
  import sys
3
3
  import socket
4
4
  import time
5
+ import tempfile
5
6
  from typing import Dict, Any, Optional, Callable
6
7
 
7
8
  from replx.utils.constants import DEFAULT_AGENT_PORT, AGENT_HOST, MAX_UDP_SIZE
@@ -327,7 +328,18 @@ class AgentClient:
327
328
  cmd.append(str(port))
328
329
 
329
330
  proc = None
331
+ stderr_file = None
332
+ stderr_path = None
333
+
330
334
  if background:
335
+ try:
336
+ fd, stderr_path = tempfile.mkstemp(prefix='replx_agent_', suffix='.err')
337
+ os.close(fd)
338
+ stderr_file = open(stderr_path, 'w', encoding='utf-8')
339
+ except Exception:
340
+ stderr_file = None
341
+ stderr_path = None
342
+
331
343
  if sys.platform == 'win32':
332
344
  startupinfo = subprocess.STARTUPINFO()
333
345
  startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
@@ -337,27 +349,48 @@ class AgentClient:
337
349
  cmd,
338
350
  creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
339
351
  stdout=subprocess.DEVNULL,
340
- stderr=subprocess.DEVNULL,
352
+ stderr=stderr_file if stderr_file else subprocess.DEVNULL,
341
353
  startupinfo=startupinfo
342
354
  )
343
355
  else:
344
356
  proc = subprocess.Popen(
345
357
  cmd,
346
358
  stdout=subprocess.DEVNULL,
347
- stderr=subprocess.DEVNULL,
359
+ stderr=stderr_file if stderr_file else subprocess.DEVNULL,
348
360
  stdin=subprocess.DEVNULL,
349
361
  start_new_session=True,
350
362
  close_fds=True
351
363
  )
364
+
365
+ if stderr_file:
366
+ stderr_file.close()
367
+ stderr_file = None
352
368
  else:
353
369
  proc = subprocess.Popen(cmd)
354
370
 
355
- for _ in range(100):
356
- time.sleep(0.1)
357
- if proc is not None and proc.poll() is not None:
358
- raise RuntimeError(f"Failed to start agent (process exited with code {proc.returncode})")
359
- if AgentClient.is_agent_running(port=port):
360
- return True
371
+ try:
372
+ for _ in range(100):
373
+ time.sleep(0.1)
374
+ if proc is not None and proc.poll() is not None:
375
+ detail = ''
376
+ if stderr_path:
377
+ try:
378
+ with open(stderr_path, 'r', encoding='utf-8', errors='replace') as f:
379
+ detail = f.read().strip()
380
+ except Exception:
381
+ pass
382
+ msg = f"Failed to start agent (process exited with code {proc.returncode})"
383
+ if detail:
384
+ msg += f"\n{detail}"
385
+ raise RuntimeError(msg)
386
+ if AgentClient.is_agent_running(port=port):
387
+ return True
388
+ finally:
389
+ if stderr_path:
390
+ try:
391
+ os.unlink(stderr_path)
392
+ except Exception:
393
+ pass
361
394
 
362
395
  raise RuntimeError("Failed to start agent (timeout)")
363
396
 
@@ -455,9 +455,6 @@ class ConnectionManager:
455
455
 
456
456
  conn = self._connections.pop(key)
457
457
 
458
- # Remember whether a detached (user) script was running before clearing the flag.
459
- # If so, the device is executing paste-mode code on its UART, NOT in raw REPL.
460
- # Sending CTRL_B in that state would inject \x02 into the running script's stdin.
461
458
  was_detached = conn.is_detached()
462
459
  conn.stop_detached()
463
460
 
@@ -474,8 +471,6 @@ class ConnectionManager:
474
471
  if transport:
475
472
  try:
476
473
  if not was_detached:
477
- # Only send CTRL_B to leave raw REPL when no user script
478
- # was running; otherwise the byte contaminates the script stdin.
479
474
  transport.write(CTRL_B)
480
475
  time.sleep(0.1 if sys.platform == 'win32' else 0.05)
481
476
  except Exception:
@@ -204,11 +204,35 @@ class ExecCommandsMixin:
204
204
  if not conn or not conn.repl_protocol:
205
205
  send_error("Not connected")
206
206
  return
207
-
207
+
208
208
  with conn.interactive.lock:
209
209
  if conn.interactive.active:
210
- send_error("Interactive session already active on this connection")
211
- return
210
+ thread = conn.interactive.thread
211
+ if thread is not None and thread.is_alive():
212
+ conn.interactive.stop_requested = True
213
+ try:
214
+ conn.repl_protocol.interrupt()
215
+ except Exception:
216
+ pass
217
+ else:
218
+ conn.interactive.active = False
219
+ conn.interactive.thread = None
220
+
221
+ for _ in range(20):
222
+ with conn.interactive.lock:
223
+ if not conn.interactive.active:
224
+ break
225
+ time.sleep(0.05)
226
+
227
+ with conn.interactive.lock:
228
+ if conn.interactive.active:
229
+ thread = conn.interactive.thread
230
+ if thread is None or not thread.is_alive():
231
+ conn.interactive.active = False
232
+ conn.interactive.thread = None
233
+ else:
234
+ send_error("Interactive session already active on this connection")
235
+ return
212
236
 
213
237
  repl = conn.repl_protocol
214
238
  if conn.is_detached():
@@ -71,7 +71,6 @@ class SessionCommandsMixin:
71
71
 
72
72
  self._add_connection(conn_key, new_conn)
73
73
 
74
- # Pass original port to session
75
74
  self.session_manager.add_connection_to_session(
76
75
  ppid, port,
77
76
  as_foreground=as_foreground,
@@ -113,7 +112,6 @@ class SessionCommandsMixin:
113
112
  ports_to_close = list(session.get_all_connections())
114
113
  freed_port = "all"
115
114
  elif port:
116
- # Find port in session
117
115
  found = None
118
116
  for conn in session.get_all_connections():
119
117
  if conn == port:
@@ -141,8 +139,6 @@ class SessionCommandsMixin:
141
139
  if not self.session_manager.has_sessions():
142
140
  def delayed_shutdown():
143
141
  time.sleep(0.3)
144
- # cleanup() properly sets _stop_event to stop the asyncio loop
145
- # and releases the UDP port so a new agent can bind it immediately.
146
142
  self.cleanup()
147
143
 
148
144
  shutdown_thread = threading.Thread(target=delayed_shutdown, daemon=True)
@@ -178,7 +174,6 @@ class SessionCommandsMixin:
178
174
 
179
175
  old_fg = session.foreground
180
176
 
181
- # Pass original port, not normalized
182
177
  session.add_connection(port, as_foreground=True)
183
178
 
184
179
  return {
@@ -189,7 +184,6 @@ class SessionCommandsMixin:
189
184
  }
190
185
 
191
186
  def _cmd_set_default(self, ctx: CommandContext, port: str = None, update_session: bool = False) -> dict:
192
- """Set default port. If update_session=True, also update calling session's default_port."""
193
187
  port = port or ctx.explicit_port
194
188
  if port:
195
189
  self._set_default_port(port)
@@ -513,7 +513,7 @@ def main():
513
513
  break
514
514
  first_nonopt = sys.argv[first_nonopt_idx] if first_nonopt_idx is not None else None
515
515
 
516
- run_opts = {'-n', '--non-interactive', '-e', '--echo', '-d', '--device'}
516
+ run_opts = {'-n', '--non-interactive', '-e', '--echo', '-d', '--device', '--line', '--hex'}
517
517
  has_device_opt = bool(run_opts & {'-d', '--device'} & set(args))
518
518
 
519
519
  script_files = []
@@ -538,7 +538,7 @@ def main():
538
538
 
539
539
  if should_inject_run:
540
540
  opt_idx = next((i for i, a in enumerate(sys.argv[1:], 1) if a in run_opts), None)
541
- insert_at = opt_idx if opt_idx is not None else script_arg_idx
541
+ insert_at = min(opt_idx, script_arg_idx) if opt_idx is not None else script_arg_idx
542
542
  sys.argv.insert(insert_at, 'run')
543
543
 
544
544
  first_nonopt_idx = next((i for i, a in enumerate(sys.argv[1:], 1) if not a.startswith('-')), None)
@@ -35,11 +35,6 @@ from ..app import app
35
35
 
36
36
 
37
37
  def _normalize_path_for_comparison(path: str) -> str:
38
- """Normalize file path for comparison.
39
-
40
- Windows: case-insensitive
41
- Linux/macOS: case-sensitive
42
- """
43
38
  path = os.path.normpath(path)
44
39
  if sys.platform.startswith("win"):
45
40
  return path.lower()
@@ -48,12 +43,6 @@ def _normalize_path_for_comparison(path: str) -> str:
48
43
 
49
44
 
50
45
  def _serial_port_cmp_key(port: str) -> str:
51
- """Comparison key for serial ports.
52
-
53
- Policy:
54
- - Display: keep OS-provided casing.
55
- - Compare: on Windows, treat COM ports case-insensitively.
56
- """
57
46
  if port is None:
58
47
  return ""
59
48
  p = str(port).strip()
@@ -61,10 +50,6 @@ def _serial_port_cmp_key(port: str) -> str:
61
50
 
62
51
 
63
52
  def _serial_port_display(port: str) -> str:
64
- """Format a serial port name for display.
65
-
66
- Requirement: on Windows, always show port names in uppercase.
67
- """
68
53
  if port is None:
69
54
  return ""
70
55
  p = str(port).strip()
@@ -72,12 +57,6 @@ def _serial_port_display(port: str) -> str:
72
57
 
73
58
 
74
59
  def _resolve_os_serial_port_name(port: str) -> str:
75
- """Resolve port name to the OS-enumerated spelling (best-effort).
76
-
77
- This is mainly for Windows where users might type `com1` but the OS reports
78
- `COM1`. We keep display as OS-provided and use case-insensitive comparison
79
- separately.
80
- """
81
60
  if not port:
82
61
  return port
83
62
  p = str(port).strip()
@@ -158,7 +137,6 @@ def _create_vscode_files_and_typehints(vscode_dir: str, core: str, device: str,
158
137
 
159
138
  extra_paths = []
160
139
 
161
- # 1. MicroPython standard library typehints (included in replx tool)
162
140
  from replx.utils.device_info import is_std_micropython
163
141
  if is_std_micropython(core):
164
142
  comm_path = StoreManager.comm_typehints_path()
@@ -167,27 +145,22 @@ def _create_vscode_files_and_typehints(vscode_dir: str, core: str, device: str,
167
145
  if comm_path and os.path.isdir(comm_path):
168
146
  extra_paths.append(comm_path)
169
147
 
170
- # 2. Core builtin typehints (included in replx tool)
171
148
  if core:
172
149
  core_builtin = StoreManager.core_typehints_path(core)
173
150
  if core_builtin and os.path.isdir(core_builtin):
174
151
  extra_paths.append(core_builtin)
175
152
 
176
- # 3. Core library typehints (downloaded via 'replx pkg download')
177
153
  if core:
178
154
  core_lib_typehints = os.path.join(StoreManager.pkg_root(), "core", core, "typehints")
179
155
  if os.path.isdir(core_lib_typehints):
180
156
  extra_paths.append(core_lib_typehints)
181
157
 
182
- # 4. Device builtin typehints (included in replx tool)
183
158
  if device:
184
159
  device_path = device_name_to_path(device)
185
160
  device_builtin = StoreManager.device_typehints_path(device_path)
186
161
  if device_builtin and os.path.isdir(device_builtin):
187
162
  extra_paths.append(device_builtin)
188
163
 
189
- # 5. Device library typehints (downloaded via 'replx pkg download')
190
- # Note: device name like 'ticle-lite' becomes 'ticle_lite' in filesystem
191
164
  if device:
192
165
  device_path = device_name_to_path(device)
193
166
  device_typehints = os.path.join(StoreManager.pkg_root(), "device", device_path, "typehints")
@@ -290,7 +263,6 @@ Run this once per project folder to set up your workspace.
290
263
  port = global_opts.get('port')
291
264
  agent_port = global_opts.get('agent_port')
292
265
 
293
- # Store/use the OS-enumerated port spelling when possible.
294
266
  port = _resolve_os_serial_port_name(port)
295
267
 
296
268
  if not port:
@@ -306,7 +278,6 @@ Run this once per project folder to set up your workspace.
306
278
 
307
279
  env_path = _find_env_file()
308
280
  if agent_port is None:
309
- # Use original port for connection lookup
310
281
  if env_path and port:
311
282
  existing_config = _get_connection_config(env_path, port)
312
283
  if existing_config and existing_config.get('agent_port'):
@@ -356,7 +327,6 @@ Run this once per project folder to set up your workspace.
356
327
  env_path = os.path.join(vscode_dir, ".replx")
357
328
  workspace = os.path.dirname(vscode_dir)
358
329
 
359
- # Use original port for storing connection
360
330
  _update_connection_config(
361
331
  env_path, port,
362
332
  version=version, core=core, device=device,
@@ -391,7 +361,6 @@ Run this once per project folder to set up your workspace.
391
361
  )
392
362
  raise typer.Exit()
393
363
  else:
394
- # Use original port for session setup
395
364
  try:
396
365
  with AgentClient(port=agent_port, device_port=port) as client:
397
366
  result = client.send_command(
@@ -410,7 +379,6 @@ Run this once per project folder to set up your workspace.
410
379
  vscode_dir = _find_or_create_vscode_dir()
411
380
  env_path = os.path.join(vscode_dir, ".replx")
412
381
 
413
- # Use original port for storing connection
414
382
  _update_connection_config(
415
383
  env_path, port,
416
384
  version=STATE.version, core=STATE.core, device=STATE.device,
@@ -478,7 +446,6 @@ Run this once per project folder to set up your workspace.
478
446
  )
479
447
  raise typer.Exit(1)
480
448
 
481
- # Use original port for session setup
482
449
  try:
483
450
  with AgentClient(port=agent_port, device_port=port) as client:
484
451
  result = client.send_command('session_setup',
@@ -542,7 +509,6 @@ Run this once per project folder to set up your workspace.
542
509
  except Exception:
543
510
  pass
544
511
 
545
- # Use original port for storing connection
546
512
  set_as_default = True
547
513
 
548
514
  if clean_mode:
@@ -1022,7 +988,6 @@ WIFI_STATUS_MESSAGES = {
1022
988
  }
1023
989
 
1024
990
  def _wifi_status(client: AgentClient):
1025
- """Show WiFi status."""
1026
991
  code = '''
1027
992
  import network
1028
993
  import json