replx 1.9__tar.gz → 1.10__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 (153) hide show
  1. {replx-1.9/replx.egg-info → replx-1.10}/PKG-INFO +1 -1
  2. {replx-1.9 → replx-1.10}/replx/__init__.py +1 -1
  3. replx-1.10/replx/__main__.py +14 -0
  4. {replx-1.9 → replx-1.10}/replx/cli/__init__.py +8 -1
  5. replx-1.10/replx/cli/agent/__init__.py +27 -0
  6. {replx-1.9 → replx-1.10}/replx/cli/agent/server/core.py +1 -1
  7. {replx-1.9 → replx-1.10}/replx/cli/app.py +110 -41
  8. {replx-1.9 → replx-1.10}/replx/cli/commands/device.py +35 -11
  9. {replx-1.9 → replx-1.10}/replx/cli/commands/exec.py +3 -3
  10. {replx-1.9 → replx-1.10}/replx/cli/commands/file.py +27 -2
  11. {replx-1.9 → replx-1.10}/replx/cli/commands/utility.py +2 -1
  12. {replx-1.9 → replx-1.10}/replx/cli/config.py +98 -3
  13. {replx-1.9 → replx-1.10}/replx/cli/helpers/__init__.py +24 -6
  14. {replx-1.9 → replx-1.10}/replx/cli/helpers/output.py +23 -7
  15. {replx-1.9 → replx-1.10}/replx/cli/helpers/scanner.py +33 -5
  16. {replx-1.9 → replx-1.10/replx.egg-info}/PKG-INFO +1 -1
  17. {replx-1.9 → replx-1.10}/replx.egg-info/SOURCES.txt +1 -0
  18. replx-1.9/replx/cli/agent/__init__.py +0 -14
  19. {replx-1.9 → replx-1.10}/LICENSE +0 -0
  20. {replx-1.9 → replx-1.10}/README.md +0 -0
  21. {replx-1.9 → replx-1.10}/pyproject.toml +0 -0
  22. {replx-1.9 → replx-1.10}/replx/cli/agent/client/__init__.py +0 -0
  23. {replx-1.9 → replx-1.10}/replx/cli/agent/client/core.py +0 -0
  24. {replx-1.9 → replx-1.10}/replx/cli/agent/client/session.py +0 -0
  25. {replx-1.9 → replx-1.10}/replx/cli/agent/protocol.py +0 -0
  26. {replx-1.9 → replx-1.10}/replx/cli/agent/server/__init__.py +0 -0
  27. {replx-1.9 → replx-1.10}/replx/cli/agent/server/__main__.py +0 -0
  28. {replx-1.9 → replx-1.10}/replx/cli/agent/server/command_dispatcher.py +0 -0
  29. {replx-1.9 → replx-1.10}/replx/cli/agent/server/connection_manager.py +0 -0
  30. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/__init__.py +0 -0
  31. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/exec.py +0 -0
  32. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/filesystem.py +0 -0
  33. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/i2c.py +0 -0
  34. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/repl.py +0 -0
  35. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/session.py +0 -0
  36. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/spi.py +0 -0
  37. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/transfer.py +0 -0
  38. {replx-1.9 → replx-1.10}/replx/cli/agent/server/handlers/uart.py +0 -0
  39. {replx-1.9 → replx-1.10}/replx/cli/agent/server/session_manager.py +0 -0
  40. {replx-1.9 → replx-1.10}/replx/cli/commands/__init__.py +0 -0
  41. {replx-1.9 → replx-1.10}/replx/cli/commands/_common.py +0 -0
  42. {replx-1.9 → replx-1.10}/replx/cli/commands/adc.py +0 -0
  43. {replx-1.9 → replx-1.10}/replx/cli/commands/firmware.py +0 -0
  44. {replx-1.9 → replx-1.10}/replx/cli/commands/gpio.py +0 -0
  45. {replx-1.9 → replx-1.10}/replx/cli/commands/i2c.py +0 -0
  46. {replx-1.9 → replx-1.10}/replx/cli/commands/package.py +0 -0
  47. {replx-1.9 → replx-1.10}/replx/cli/commands/pwm.py +0 -0
  48. {replx-1.9 → replx-1.10}/replx/cli/commands/spi.py +0 -0
  49. {replx-1.9 → replx-1.10}/replx/cli/commands/uart.py +0 -0
  50. {replx-1.9 → replx-1.10}/replx/cli/commands/wifi.py +0 -0
  51. {replx-1.9 → replx-1.10}/replx/cli/connection.py +0 -0
  52. {replx-1.9 → replx-1.10}/replx/cli/helpers/compiler.py +0 -0
  53. {replx-1.9 → replx-1.10}/replx/cli/helpers/environment.py +0 -0
  54. {replx-1.9 → replx-1.10}/replx/cli/helpers/registry.py +0 -0
  55. {replx-1.9 → replx-1.10}/replx/cli/helpers/store.py +0 -0
  56. {replx-1.9 → replx-1.10}/replx/cli/helpers/updater.py +0 -0
  57. {replx-1.9 → replx-1.10}/replx/commands.py +0 -0
  58. {replx-1.9 → replx-1.10}/replx/protocol/__init__.py +0 -0
  59. {replx-1.9 → replx-1.10}/replx/protocol/repl.py +0 -0
  60. {replx-1.9 → replx-1.10}/replx/protocol/storage.py +0 -0
  61. {replx-1.9 → replx-1.10}/replx/terminal.py +0 -0
  62. {replx-1.9 → replx-1.10}/replx/transport/__init__.py +0 -0
  63. {replx-1.9 → replx-1.10}/replx/transport/serial.py +0 -0
  64. {replx-1.9 → replx-1.10}/replx/typehints/comm/_thread.pyi +0 -0
  65. {replx-1.9 → replx-1.10}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  66. {replx-1.9 → replx-1.10}/replx/typehints/comm/array.pyi +0 -0
  67. {replx-1.9 → replx-1.10}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  68. {replx-1.9 → replx-1.10}/replx/typehints/comm/binascii.pyi +0 -0
  69. {replx-1.9 → replx-1.10}/replx/typehints/comm/bluetooth.pyi +0 -0
  70. {replx-1.9 → replx-1.10}/replx/typehints/comm/builtins.pyi +0 -0
  71. {replx-1.9 → replx-1.10}/replx/typehints/comm/cmath.pyi +0 -0
  72. {replx-1.9 → replx-1.10}/replx/typehints/comm/collections.pyi +0 -0
  73. {replx-1.9 → replx-1.10}/replx/typehints/comm/cryptolib.pyi +0 -0
  74. {replx-1.9 → replx-1.10}/replx/typehints/comm/deflate.pyi +0 -0
  75. {replx-1.9 → replx-1.10}/replx/typehints/comm/errno.pyi +0 -0
  76. {replx-1.9 → replx-1.10}/replx/typehints/comm/framebuf.pyi +0 -0
  77. {replx-1.9 → replx-1.10}/replx/typehints/comm/gc.pyi +0 -0
  78. {replx-1.9 → replx-1.10}/replx/typehints/comm/hashlib.pyi +0 -0
  79. {replx-1.9 → replx-1.10}/replx/typehints/comm/heapq.pyi +0 -0
  80. {replx-1.9 → replx-1.10}/replx/typehints/comm/io.pyi +0 -0
  81. {replx-1.9 → replx-1.10}/replx/typehints/comm/json.pyi +0 -0
  82. {replx-1.9 → replx-1.10}/replx/typehints/comm/lwip.pyi +0 -0
  83. {replx-1.9 → replx-1.10}/replx/typehints/comm/machine.pyi +0 -0
  84. {replx-1.9 → replx-1.10}/replx/typehints/comm/math.pyi +0 -0
  85. {replx-1.9 → replx-1.10}/replx/typehints/comm/micropython.pyi +0 -0
  86. {replx-1.9 → replx-1.10}/replx/typehints/comm/mip/__init__.pyi +0 -0
  87. {replx-1.9 → replx-1.10}/replx/typehints/comm/network.pyi +0 -0
  88. {replx-1.9 → replx-1.10}/replx/typehints/comm/ntptime.pyi +0 -0
  89. {replx-1.9 → replx-1.10}/replx/typehints/comm/os.pyi +0 -0
  90. {replx-1.9 → replx-1.10}/replx/typehints/comm/platform.pyi +0 -0
  91. {replx-1.9 → replx-1.10}/replx/typehints/comm/random.pyi +0 -0
  92. {replx-1.9 → replx-1.10}/replx/typehints/comm/re.pyi +0 -0
  93. {replx-1.9 → replx-1.10}/replx/typehints/comm/requests/__init__.pyi +0 -0
  94. {replx-1.9 → replx-1.10}/replx/typehints/comm/select.pyi +0 -0
  95. {replx-1.9 → replx-1.10}/replx/typehints/comm/socket.pyi +0 -0
  96. {replx-1.9 → replx-1.10}/replx/typehints/comm/ssl.pyi +0 -0
  97. {replx-1.9 → replx-1.10}/replx/typehints/comm/struct.pyi +0 -0
  98. {replx-1.9 → replx-1.10}/replx/typehints/comm/sys.pyi +0 -0
  99. {replx-1.9 → replx-1.10}/replx/typehints/comm/time.pyi +0 -0
  100. {replx-1.9 → replx-1.10}/replx/typehints/comm/tls.pyi +0 -0
  101. {replx-1.9 → replx-1.10}/replx/typehints/comm/uasyncio.pyi +0 -0
  102. {replx-1.9 → replx-1.10}/replx/typehints/comm/uctypes.pyi +0 -0
  103. {replx-1.9 → replx-1.10}/replx/typehints/comm/urequests.pyi +0 -0
  104. {replx-1.9 → replx-1.10}/replx/typehints/comm/vfs.pyi +0 -0
  105. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  106. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/digi/__init__.pyi +0 -0
  107. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/digi/ble.pyi +0 -0
  108. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  109. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  110. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  111. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  112. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  113. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  114. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  115. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  116. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  117. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  118. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  119. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  120. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  121. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  122. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  123. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  124. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  125. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  126. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  127. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  128. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  129. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  130. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  131. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  132. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  133. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  134. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  135. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  136. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/xbee/__init__.pyi +0 -0
  137. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/xbee/modem_status.pyi +0 -0
  138. {replx-1.9 → replx-1.10}/replx/typehints/comm_separate/EFR32MG/xbee/relay.pyi +0 -0
  139. {replx-1.9 → replx-1.10}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  140. {replx-1.9 → replx-1.10}/replx/typehints/core/ESP32/esp.pyi +0 -0
  141. {replx-1.9 → replx-1.10}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  142. {replx-1.9 → replx-1.10}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  143. {replx-1.9 → replx-1.10}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  144. {replx-1.9 → replx-1.10}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  145. {replx-1.9 → replx-1.10}/replx/utils/__init__.py +0 -0
  146. {replx-1.9 → replx-1.10}/replx/utils/constants.py +0 -0
  147. {replx-1.9 → replx-1.10}/replx/utils/device_info.py +0 -0
  148. {replx-1.9 → replx-1.10}/replx/utils/exceptions.py +0 -0
  149. {replx-1.9 → replx-1.10}/replx.egg-info/dependency_links.txt +0 -0
  150. {replx-1.9 → replx-1.10}/replx.egg-info/entry_points.txt +0 -0
  151. {replx-1.9 → replx-1.10}/replx.egg-info/requires.txt +0 -0
  152. {replx-1.9 → replx-1.10}/replx.egg-info/top_level.txt +0 -0
  153. {replx-1.9 → replx-1.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replx
3
- Version: 1.9
3
+ Version: 1.10
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.9"
2
+ __version__ = "1.10"
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
 
@@ -0,0 +1,14 @@
1
+ """Allow ``python -m replx ...`` as a fast-startup alias for the ``replx`` script.
2
+
3
+ On Windows the ``replx.exe`` console-script launcher (installed by pip from
4
+ ``[project.scripts]``) goes through a zipapp-style entry that adds ~450ms to
5
+ every invocation. ``python -m replx`` bypasses that launcher and is noticeably
6
+ snappier for users who care about CLI startup latency.
7
+ """
8
+
9
+ import sys
10
+
11
+ from replx.cli.app import main
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(main())
@@ -3,7 +3,6 @@ from .config import (
3
3
  ConfigManager, AgentPortManager, ConnectionResolver,
4
4
  )
5
5
  from replx.utils.constants import DEFAULT_AGENT_PORT, MIN_AGENT_PORT, MAX_AGENT_PORT
6
- from .app import app, main
7
6
 
8
7
  __all__ = [
9
8
  'RuntimeState', 'STATE', 'GLOBAL_OPTIONS',
@@ -11,3 +10,11 @@ __all__ = [
11
10
  'DEFAULT_AGENT_PORT', 'MIN_AGENT_PORT', 'MAX_AGENT_PORT',
12
11
  'app', 'main'
13
12
  ]
13
+
14
+
15
+ def __getattr__(name):
16
+ if name in {'app', 'main'}:
17
+ from .app import app, main
18
+
19
+ return {'app': app, 'main': main}[name]
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,27 @@
1
+
2
+ from .protocol import AgentProtocol
3
+ from .client import AgentClient, get_session_id, get_cached_session_id, clear_session_cache
4
+
5
+ __all__ = [
6
+ 'AgentProtocol',
7
+ 'AgentClient',
8
+ 'get_session_id',
9
+ 'get_cached_session_id',
10
+ 'clear_session_cache',
11
+ 'AgentServer',
12
+ 'agent_main',
13
+ ]
14
+
15
+
16
+ def __getattr__(name):
17
+ # The server stack pulls in ``replx.terminal`` (~150ms) plus all command
18
+ # handlers; CLI clients never need it. Defer until something actually asks.
19
+ if name == 'AgentServer':
20
+ from .server import AgentServer
21
+
22
+ return AgentServer
23
+ if name == 'agent_main':
24
+ from .server import main as agent_main
25
+
26
+ return agent_main
27
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -334,7 +334,7 @@ class AgentServer(
334
334
  )
335
335
  self._datagram_transport = transport
336
336
  self.running = True
337
- print(f'replx agent started listening on {AGENT_HOST}:{self.agent_port} (UDP)')
337
+ print(f'replx agent started - listening on {AGENT_HOST}:{self.agent_port} (UDP)')
338
338
 
339
339
  heartbeat_task = asyncio.create_task(
340
340
  self._heartbeat_coro(), name='replx-heartbeat'
@@ -1,13 +1,14 @@
1
1
  import sys
2
+ import importlib
2
3
  from typing import Optional
3
4
  import typer
4
5
  from rich.panel import Panel
5
6
 
7
+ if __name__ == '__main__':
8
+ sys.modules['replx.cli.app'] = sys.modules[__name__]
9
+
6
10
  from replx import __version__
7
11
  from .helpers.output import OutputHelper, get_panel_box, CONSOLE_WIDTH
8
- from .helpers.updater import UpdateChecker
9
- from .helpers.environment import EnvironmentManager
10
- from .connection import _ensure_connected, _create_agent_client
11
12
  from .config import STATE, _set_global_options, _find_env_file, _get_theme_config
12
13
 
13
14
 
@@ -118,32 +119,43 @@ def _get_console():
118
119
  return OutputHelper.make_console(width=CONSOLE_WIDTH)
119
120
 
120
121
 
121
- try:
122
- import typer.rich_utils
123
-
124
- def _patched_get_rich_console():
125
- return _get_console()
126
-
127
- typer.rich_utils._get_rich_console = _patched_get_rich_console
128
- except ImportError:
129
- pass
122
+ _RICH_HELP_CONFIGURED = False
130
123
 
131
- try:
132
- from typer.core import RichCommand
133
- _original_rich_command_format_help = RichCommand.format_help
134
-
135
- def _format_help_width(self, ctx, formatter):
136
- old_console = getattr(self, '_rich_console', None)
137
- try:
138
- self._rich_console = OutputHelper.make_console(width=CONSOLE_WIDTH)
139
- return _original_rich_command_format_help(self, ctx, formatter)
140
- finally:
141
- if old_console is not None:
142
- self._rich_console = old_console
143
-
144
- RichCommand.format_help = _format_help_width
145
- except ImportError:
146
- pass
124
+
125
+ def _configure_rich_help_rendering() -> None:
126
+ global _RICH_HELP_CONFIGURED
127
+ if _RICH_HELP_CONFIGURED:
128
+ return
129
+
130
+ try:
131
+ import typer.rich_utils
132
+
133
+ def _patched_get_rich_console():
134
+ return _get_console()
135
+
136
+ typer.rich_utils._get_rich_console = _patched_get_rich_console
137
+ except ImportError:
138
+ pass
139
+
140
+ try:
141
+ from typer.core import RichCommand
142
+
143
+ original_format_help = RichCommand.format_help
144
+
145
+ def _format_help_width(self, ctx, formatter):
146
+ old_console = getattr(self, '_rich_console', None)
147
+ try:
148
+ self._rich_console = OutputHelper.make_console(width=CONSOLE_WIDTH)
149
+ return original_format_help(self, ctx, formatter)
150
+ finally:
151
+ if old_console is not None:
152
+ self._rich_console = old_console
153
+
154
+ RichCommand.format_help = _format_help_width
155
+ except ImportError:
156
+ pass
157
+
158
+ _RICH_HELP_CONFIGURED = True
147
159
 
148
160
 
149
161
  import click
@@ -303,6 +315,64 @@ app = typer.Typer(
303
315
  )
304
316
 
305
317
 
318
+ _COMMAND_MODULES = {
319
+ 'get': 'file',
320
+ 'cat': 'file',
321
+ 'put': 'file',
322
+ 'ls': 'file',
323
+ 'cp': 'file',
324
+ 'mv': 'file',
325
+ 'rm': 'file',
326
+ 'mkdir': 'file',
327
+ 'touch': 'file',
328
+ 'setup': 'device',
329
+ 'usage': 'device',
330
+ 'scan': 'device',
331
+ 'exec': 'exec',
332
+ 'run': 'exec',
333
+ 'repl': 'exec',
334
+ 'shell': 'exec',
335
+ 'reset': 'exec',
336
+ 'pkg': 'package',
337
+ 'mpy': 'package',
338
+ 'version': 'utility',
339
+ 'status': 'utility',
340
+ 'fg': 'utility',
341
+ 'whoami': 'utility',
342
+ 'disconnect': 'utility',
343
+ 'shutdown': 'utility',
344
+ 'format': 'utility',
345
+ 'init': 'utility',
346
+ 'firmware': 'firmware',
347
+ 'i2c': 'i2c',
348
+ 'gpio': 'gpio',
349
+ 'adc': 'adc',
350
+ 'pwm': 'pwm',
351
+ 'uart': 'uart',
352
+ 'spi': 'spi',
353
+ 'wifi': 'wifi',
354
+ }
355
+ _KNOWN_COMMANDS = set(_COMMAND_MODULES) | {'connect', 'help'}
356
+ _LOADED_COMMAND_MODULES: set[str] = set()
357
+
358
+
359
+ def _load_command_module(module_name: str) -> None:
360
+ if module_name in _LOADED_COMMAND_MODULES:
361
+ return
362
+
363
+ importlib.import_module(f'replx.cli.commands.{module_name}')
364
+ _LOADED_COMMAND_MODULES.add(module_name)
365
+
366
+
367
+ def _load_required_command_module(command_name: Optional[str]) -> None:
368
+ if not command_name:
369
+ return
370
+
371
+ module_name = _COMMAND_MODULES.get(command_name)
372
+ if module_name:
373
+ _load_command_module(module_name)
374
+
375
+
306
376
  def _print_main_help():
307
377
  lines = []
308
378
  lines.append("[bold]MicroPython development tool for VSCode[/bold]")
@@ -384,16 +454,7 @@ def _print_main_help():
384
454
 
385
455
 
386
456
  def _get_known_commands() -> set[str]:
387
- known = {'connect', 'help', 'version'}
388
- for command_info in getattr(app, 'registered_commands', []):
389
- name = getattr(command_info, 'name', None)
390
- if name:
391
- known.add(name)
392
- callback = getattr(command_info, 'callback', None)
393
- callback_name = getattr(callback, '__name__', None)
394
- if callback_name:
395
- known.add(callback_name)
396
- return known
457
+ return set(_KNOWN_COMMANDS)
397
458
 
398
459
 
399
460
  @app.callback(invoke_without_command=True)
@@ -426,9 +487,6 @@ def cli(
426
487
  _print_main_help()
427
488
  raise typer.Exit()
428
489
 
429
-
430
- from .commands import file, device, exec, package, utility, firmware, i2c, gpio, adc, pwm, uart, spi, wifi
431
-
432
490
  def main():
433
491
  if len(sys.argv) == 1:
434
492
  OutputHelper.print_panel(
@@ -462,6 +520,8 @@ def main():
462
520
  command_str = sys.argv[2]
463
521
 
464
522
  try:
523
+ from .connection import _ensure_connected, _create_agent_client
524
+
465
525
  _ensure_connected()
466
526
  client = _create_agent_client()
467
527
  result = client.send_command('exec', code=command_str)
@@ -559,12 +619,21 @@ def main():
559
619
  first_nonopt_idx = next((i for i, a in enumerate(sys.argv[1:], 1) if not a.startswith('-')), None)
560
620
  first_nonopt = sys.argv[first_nonopt_idx] if first_nonopt_idx is not None else None
561
621
 
622
+ _load_required_command_module(first_nonopt)
623
+
562
624
  suppressed = {'scan'}
625
+ if any(x in sys.argv for x in ('--help', '-h')):
626
+ _configure_rich_help_rendering()
627
+
563
628
  if not any(x in sys.argv for x in ('--help','-h','--version','-v')):
564
629
  if (first_nonopt is None) or (first_nonopt not in suppressed):
630
+ from .helpers.updater import UpdateChecker
631
+
565
632
  UpdateChecker.check_for_updates(__version__)
566
633
 
567
634
  try:
635
+ from .helpers.environment import EnvironmentManager
636
+
568
637
  EnvironmentManager.load_env_from_rep()
569
638
  app(standalone_mode=False)
570
639
  exit_code = 0
@@ -23,7 +23,7 @@ from ..config import (
23
23
  _find_env_file, _find_or_create_vscode_dir,
24
24
  _read_env_ini,
25
25
  _update_connection_config, _find_available_agent_port, _find_running_agent_ports,
26
- _get_global_options
26
+ _get_global_options, AgentPortManager
27
27
  )
28
28
  from ..connection import (
29
29
  _ensure_connected, _create_agent_client
@@ -141,17 +141,30 @@ def _load_jsonc(path: str) -> dict | None:
141
141
 
142
142
 
143
143
  def _get_portable_vscode_root_from_pshome() -> str | None:
144
+ cached_root = AgentPortManager._read_cached_vscode_root()
145
+ if cached_root:
146
+ return cached_root
147
+
144
148
  pshome = os.environ.get("PSHOME", "").strip()
145
149
  if not pshome:
146
150
  try:
151
+ run_kwargs = {
152
+ "capture_output": True,
153
+ "text": True,
154
+ "encoding": "utf-8",
155
+ "errors": "replace",
156
+ "timeout": 2,
157
+ "check": False,
158
+ }
159
+ if sys.platform.startswith("win"):
160
+ startupinfo = subprocess.STARTUPINFO()
161
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
162
+ startupinfo.wShowWindow = 0
163
+ run_kwargs["startupinfo"] = startupinfo
164
+ run_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
147
165
  result = subprocess.run(
148
166
  ["pwsh", "-NoLogo", "-NoProfile", "-Command", "$PSHOME"],
149
- capture_output=True,
150
- text=True,
151
- encoding="utf-8",
152
- errors="replace",
153
- timeout=2,
154
- check=False,
167
+ **run_kwargs,
155
168
  )
156
169
  pshome = result.stdout.strip()
157
170
  except Exception:
@@ -160,7 +173,9 @@ def _get_portable_vscode_root_from_pshome() -> str | None:
160
173
  if not pshome:
161
174
  return None
162
175
 
163
- return os.path.dirname(os.path.dirname(os.path.dirname(pshome)))
176
+ vscode_root = os.path.dirname(os.path.dirname(os.path.dirname(pshome)))
177
+ AgentPortManager._write_cached_vscode_root(vscode_root)
178
+ return vscode_root
164
179
 
165
180
 
166
181
  def _map_vscode_theme_to_replx(vscode_theme: str | None) -> str | None:
@@ -381,6 +396,12 @@ Run this once per project folder to set up your workspace.
381
396
  )
382
397
  raise typer.Exit(1)
383
398
 
399
+ stored_theme = selected_theme
400
+ theme_mode = None
401
+ if auto_theme and not theme:
402
+ stored_theme = display_theme
403
+ theme_mode = 'vscode-auto'
404
+
384
405
  if clean:
385
406
  _port = _get_global_options().get('port')
386
407
  if _port:
@@ -476,7 +497,8 @@ Run this once per project folder to set up your workspace.
476
497
  env_path, port,
477
498
  version=version, core=core, device=device,
478
499
  manufacturer=manufacturer,
479
- theme=selected_theme,
500
+ theme=stored_theme,
501
+ theme_mode=theme_mode,
480
502
  set_default=True
481
503
  )
482
504
 
@@ -542,7 +564,8 @@ Run this once per project folder to set up your workspace.
542
564
  env_path, port,
543
565
  version=STATE.version, core=STATE.core, device=STATE.device,
544
566
  manufacturer=STATE.manufacturer,
545
- theme=selected_theme,
567
+ theme=stored_theme,
568
+ theme_mode=theme_mode,
546
569
  set_default=True
547
570
  )
548
571
 
@@ -680,7 +703,8 @@ Run this once per project folder to set up your workspace.
680
703
  core=STATE.core,
681
704
  device=STATE.device,
682
705
  manufacturer=STATE.manufacturer,
683
- theme=selected_theme,
706
+ theme=stored_theme,
707
+ theme_mode=theme_mode,
684
708
  set_default=set_as_default
685
709
  )
686
710
 
@@ -29,9 +29,6 @@ from ..connection import (
29
29
 
30
30
  from ..app import app
31
31
 
32
- from .file import ls, cat, cp, mv, rm, mkdir, touch
33
- from .device import usage
34
-
35
32
 
36
33
  def _wrap_trailing_expression_for_print(code: str) -> str:
37
34
  try:
@@ -1637,6 +1634,9 @@ Use familiar commands (ls, cd, cat, etc.) without typing "replx" each time.
1637
1634
  raise typer.Exit()
1638
1635
 
1639
1636
  _ensure_connected()
1637
+
1638
+ from .device import usage
1639
+ from .file import cat, cp, ls, mkdir, mv, rm, touch
1640
1640
 
1641
1641
  SHELL_COMMANDS = {
1642
1642
  'ls', 'cat', 'cp', 'mv', 'rm', 'mkdir', 'touch', 'usage', 'exec', 'repl', 'run',
@@ -1799,6 +1799,16 @@ Upload files or directories from your computer to the device.
1799
1799
 
1800
1800
  client = _create_agent_client()
1801
1801
 
1802
+ def _exit_upload_error(error: RuntimeError) -> None:
1803
+ if OutputHelper.handle_error(error, "Upload"):
1804
+ raise typer.Exit(1)
1805
+ OutputHelper.print_panel(
1806
+ f"Upload failed: [red]{error}[/red]",
1807
+ title="Upload Failed",
1808
+ border_style="red"
1809
+ )
1810
+ raise typer.Exit(1)
1811
+
1802
1812
  def ensure_remote_dir_tree(dir_path: str) -> None:
1803
1813
  normalized = OutputHelper.normalize_remote_path(dir_path).rstrip('/')
1804
1814
  if not normalized:
@@ -1837,7 +1847,10 @@ Upload files or directories from your computer to the device.
1837
1847
 
1838
1848
  target_dir = remote if is_remote_dir else posixpath.dirname(remote_path)
1839
1849
  if target_dir and target_dir != '/':
1840
- ensure_remote_dir_tree(target_dir)
1850
+ try:
1851
+ ensure_remote_dir_tree(target_dir)
1852
+ except RuntimeError as e:
1853
+ _exit_upload_error(e)
1841
1854
 
1842
1855
  display_remote = remote_path.replace(device_root_fs, "", 1)
1843
1856
  item_type = "Directory" if is_dir else "File"
@@ -1914,6 +1927,8 @@ Upload files or directories from your computer to the device.
1914
1927
  border_style="green"
1915
1928
  )
1916
1929
  except Exception as e:
1930
+ if isinstance(e, RuntimeError):
1931
+ _exit_upload_error(e)
1917
1932
  OutputHelper.print_panel(
1918
1933
  f"Upload failed: [red]{str(e)}[/red]",
1919
1934
  title="Upload Failed",
@@ -1930,7 +1945,10 @@ Upload files or directories from your computer to the device.
1930
1945
  raise typer.Exit(1)
1931
1946
 
1932
1947
  if remote and remote != '/':
1933
- ensure_remote_dir_tree(remote)
1948
+ try:
1949
+ ensure_remote_dir_tree(remote)
1950
+ except RuntimeError as e:
1951
+ _exit_upload_error(e)
1934
1952
 
1935
1953
  def count_bytes(path):
1936
1954
  if os.path.isfile(path):
@@ -2051,6 +2069,13 @@ Upload files or directories from your computer to the device.
2051
2069
  ))
2052
2070
  break
2053
2071
 
2072
+ except RuntimeError as e:
2073
+ if retry == max_retries - 1:
2074
+ if OutputHelper.handle_error(e, f"Upload: {remote_path}"):
2075
+ raise typer.Exit(1)
2076
+ OutputHelper._console.print(f"[red]Failed to upload {base_name}: {str(e)}[/red]")
2077
+ else:
2078
+ time.sleep(0.2)
2054
2079
  except Exception as e:
2055
2080
  if retry == max_retries - 1:
2056
2081
  OutputHelper._console.print(f"[red]Failed to upload {base_name}: {str(e)}[/red]")
@@ -29,7 +29,6 @@ from ..connection import (
29
29
  _get_current_agent_port
30
30
  )
31
31
  from ..app import app
32
- from .package import _install_spec_internal
33
32
 
34
33
 
35
34
  def _port_sort_key(port: str) -> tuple:
@@ -1291,6 +1290,8 @@ Completely reset device: format filesystem and install all libraries.
1291
1290
 
1292
1291
  try:
1293
1292
  specs_to_install = ["core.all"]
1293
+
1294
+ from .package import _install_spec_internal
1294
1295
 
1295
1296
  dev_src = os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(STATE.device), "src")
1296
1297
  if os.path.isdir(dev_src):
@@ -204,6 +204,7 @@ class ConfigManager:
204
204
  serial_port: str = None,
205
205
  agent_port: int = None,
206
206
  theme: str = None,
207
+ theme_mode: str = None,
207
208
  set_default: bool = False):
208
209
 
209
210
  def _resolve_os_serial_port_name(port: str) -> str:
@@ -286,6 +287,7 @@ class ConfigManager:
286
287
 
287
288
  if theme is not None:
288
289
  AgentPortManager._write_registered_theme(theme)
290
+ AgentPortManager._write_registered_theme_mode(theme_mode)
289
291
 
290
292
  ConfigManager.write(env_path, env_data['connections'], env_data['default'])
291
293
 
@@ -296,18 +298,25 @@ class ConfigManager:
296
298
 
297
299
  @staticmethod
298
300
  def get_theme(env_path: str | None = None) -> str:
301
+ theme_mode = AgentPortManager._read_registered_theme_mode()
302
+ if theme_mode:
303
+ return theme_mode
304
+
299
305
  theme = AgentPortManager._read_registered_theme(legacy_env_path=env_path)
300
306
  return theme or 'dark'
301
307
 
302
308
  @staticmethod
303
- def set_theme(env_path: str | None, theme: str):
309
+ def set_theme(env_path: str | None, theme: str, theme_mode: str | None = None):
304
310
  AgentPortManager._write_registered_theme(theme)
311
+ AgentPortManager._write_registered_theme_mode(theme_mode)
305
312
 
306
313
 
307
314
  class AgentPortManager:
308
315
 
309
316
  _AGENT_PORT_KEY = 'AGENT_PORT'
310
317
  _THEME_KEY = 'THEME'
318
+ _THEME_MODE_KEY = 'THEME_MODE'
319
+ _VSCODE_ROOT_KEY = 'VSCODE_ROOT'
311
320
 
312
321
  @staticmethod
313
322
  def _kill_agent_process_by_port(port: int) -> bool:
@@ -341,20 +350,49 @@ class AgentPortManager:
341
350
 
342
351
  return min(running_ports)
343
352
 
353
+ _singleton_cache: Optional[int] = None
354
+ _singleton_cache_miss: bool = False
355
+
344
356
  @staticmethod
345
357
  def _ensure_singleton_running_agent(preferred_port: Optional[int] = None) -> Optional[int]:
346
358
  from .agent.client import AgentClient
347
359
 
360
+ # Process-level cache: ``_ensure_connected`` calls this twice in the same
361
+ # CLI invocation. The set of running agents cannot change between those
362
+ # back-to-back calls, so reuse the prior result.
363
+ if AgentPortManager._singleton_cache is not None:
364
+ return AgentPortManager._singleton_cache
365
+ if AgentPortManager._singleton_cache_miss:
366
+ return None
367
+
368
+ # Fast path: if the preferred / registered / default port already responds
369
+ # to a ping, skip the slow scan-and-cleanup phase. Stale agent processes
370
+ # left from previous sessions can otherwise add ~0.3s per dead candidate
371
+ # (UDP probe timeout) on every CLI invocation. Singleton enforcement and
372
+ # zombie cleanup still happen on cold paths (setup / shutdown) where the
373
+ # priority candidate is unreachable.
374
+ registered_port = AgentPortManager._read_registered_port()
375
+ for candidate in (preferred_port, registered_port, DEFAULT_AGENT_PORT):
376
+ if not isinstance(candidate, int):
377
+ continue
378
+ if AgentClient.is_agent_running(port=candidate):
379
+ AgentPortManager._singleton_cache = candidate
380
+ if registered_port != candidate:
381
+ AgentPortManager._write_registered_port(candidate)
382
+ return candidate
383
+
348
384
  running_ports = []
349
385
  for port in AgentPortManager._get_candidate_agent_ports(preferred_port=preferred_port):
350
386
  if AgentClient.is_agent_running(port=port):
351
387
  running_ports.append(port)
352
388
 
353
389
  if not running_ports:
390
+ AgentPortManager._singleton_cache_miss = True
354
391
  return None
355
392
 
356
393
  primary_port = AgentPortManager._pick_primary_port(running_ports, preferred_port=preferred_port)
357
394
  if primary_port is None:
395
+ AgentPortManager._singleton_cache_miss = True
358
396
  return None
359
397
 
360
398
  for port in running_ports:
@@ -382,6 +420,7 @@ class AgentPortManager:
382
420
  )
383
421
 
384
422
  AgentPortManager._write_registered_port(primary_port)
423
+ AgentPortManager._singleton_cache = primary_port
385
424
  return primary_port
386
425
 
387
426
  @staticmethod
@@ -482,6 +521,14 @@ class AgentPortManager:
482
521
 
483
522
  return None
484
523
 
524
+ @staticmethod
525
+ def _read_registered_theme_mode() -> Optional[str]:
526
+ config_entries = AgentPortManager._read_agent_config()
527
+ theme_mode = config_entries.get(AgentPortManager._THEME_MODE_KEY)
528
+ if theme_mode:
529
+ return theme_mode.strip() or None
530
+ return None
531
+
485
532
  @staticmethod
486
533
  def _write_registered_theme(theme: Optional[str]) -> None:
487
534
  if theme is None:
@@ -495,6 +542,54 @@ class AgentPortManager:
495
542
  entries[AgentPortManager._THEME_KEY] = normalized
496
543
  AgentPortManager._write_agent_config(entries)
497
544
 
545
+ @staticmethod
546
+ def _write_registered_theme_mode(theme_mode: Optional[str]) -> None:
547
+ entries = AgentPortManager._read_agent_config()
548
+
549
+ if theme_mode is None:
550
+ entries.pop(AgentPortManager._THEME_MODE_KEY, None)
551
+ AgentPortManager._write_agent_config(entries)
552
+ return
553
+
554
+ normalized = str(theme_mode).strip()
555
+ if not normalized:
556
+ entries.pop(AgentPortManager._THEME_MODE_KEY, None)
557
+ AgentPortManager._write_agent_config(entries)
558
+ return
559
+
560
+ entries[AgentPortManager._THEME_MODE_KEY] = normalized
561
+ AgentPortManager._write_agent_config(entries)
562
+
563
+ @staticmethod
564
+ def _read_cached_vscode_root() -> Optional[str]:
565
+ entries = AgentPortManager._read_agent_config()
566
+ root = entries.get(AgentPortManager._VSCODE_ROOT_KEY)
567
+ if not root:
568
+ return None
569
+
570
+ settings_path = os.path.join(root, 'data', 'user-data', 'User', 'settings.json')
571
+ if os.path.exists(settings_path):
572
+ return root
573
+
574
+ return None
575
+
576
+ @staticmethod
577
+ def _write_cached_vscode_root(root: Optional[str]) -> None:
578
+ if root is None:
579
+ return
580
+
581
+ normalized = str(root).strip()
582
+ if not normalized:
583
+ return
584
+
585
+ settings_path = os.path.join(normalized, 'data', 'user-data', 'User', 'settings.json')
586
+ if not os.path.exists(settings_path):
587
+ return
588
+
589
+ entries = AgentPortManager._read_agent_config()
590
+ entries[AgentPortManager._VSCODE_ROOT_KEY] = normalized
591
+ AgentPortManager._write_agent_config(entries)
592
+
498
593
  @staticmethod
499
594
  def _can_bind_port(port: Optional[int]) -> bool:
500
595
  if not isinstance(port, int) or not (MIN_AGENT_PORT <= port <= MAX_AGENT_PORT):
@@ -732,8 +827,8 @@ def _get_default_connection(env_path: str) -> Optional[str]:
732
827
  def _get_theme_config(env_path: str | None = None) -> str:
733
828
  return ConfigManager.get_theme(env_path)
734
829
 
735
- def _set_theme_config(env_path: str | None, theme: str):
736
- return ConfigManager.set_theme(env_path, theme)
830
+ def _set_theme_config(env_path: str | None, theme: str, theme_mode: str | None = None):
831
+ return ConfigManager.set_theme(env_path, theme, theme_mode)
737
832
 
738
833
  def _find_available_agent_port(env_path: str) -> int:
739
834
  return AgentPortManager.find_available_port(env_path)