replx-agent 2.0.0__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.
Potentially problematic release.
This version of replx-agent might be problematic. Click here for more details.
- replx_agent/__init__.py +3 -0
- replx_agent/__main__.py +5 -0
- replx_agent/commands.py +131 -0
- replx_agent/helpers/__init__.py +3 -0
- replx_agent/helpers/scanner.py +308 -0
- replx_agent/pkg/__init__.py +11 -0
- replx_agent/pkg/compiler.py +153 -0
- replx_agent/pkg/registry.py +387 -0
- replx_agent/pkg/service.py +690 -0
- replx_agent/pkg/store.py +83 -0
- replx_agent/protocol/__init__.py +11 -0
- replx_agent/protocol/repl.py +1029 -0
- replx_agent/protocol/storage.py +1018 -0
- replx_agent/rpc/__init__.py +3 -0
- replx_agent/rpc/protocol.py +220 -0
- replx_agent/server/__init__.py +3 -0
- replx_agent/server/__main__.py +4 -0
- replx_agent/server/command_dispatcher.py +49 -0
- replx_agent/server/connection_manager.py +654 -0
- replx_agent/server/core.py +1221 -0
- replx_agent/server/handlers/__init__.py +24 -0
- replx_agent/server/handlers/exec.py +553 -0
- replx_agent/server/handlers/filesystem.py +383 -0
- replx_agent/server/handlers/firmware.py +118 -0
- replx_agent/server/handlers/i2c.py +23 -0
- replx_agent/server/handlers/package.py +182 -0
- replx_agent/server/handlers/repl.py +188 -0
- replx_agent/server/handlers/session.py +205 -0
- replx_agent/server/handlers/spi.py +23 -0
- replx_agent/server/handlers/transfer.py +600 -0
- replx_agent/server/handlers/uart.py +32 -0
- replx_agent/server/session_manager.py +391 -0
- replx_agent/server/tcp.py +207 -0
- replx_agent/terminal.py +566 -0
- replx_agent/transport/__init__.py +32 -0
- replx_agent/transport/serial.py +220 -0
- replx_agent/utils/__init__.py +95 -0
- replx_agent/utils/constants.py +78 -0
- replx_agent/utils/device_info.py +183 -0
- replx_agent/utils/exceptions.py +31 -0
- replx_agent-2.0.0.dist-info/METADATA +74 -0
- replx_agent-2.0.0.dist-info/RECORD +46 -0
- replx_agent-2.0.0.dist-info/WHEEL +5 -0
- replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
- replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- replx_agent-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import urllib.request
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from replx_agent.utils import device_name_to_path
|
|
6
|
+
from replx_agent.utils.constants import HTTP_REQUEST_TIMEOUT
|
|
7
|
+
from replx_agent.utils.exceptions import PkgError
|
|
8
|
+
from .store import StoreManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InstallHelper:
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def resolve_spec(spec: str) -> Tuple[str, str]:
|
|
15
|
+
if spec in ("core/", "device/", "core", "device"):
|
|
16
|
+
raise PkgError(
|
|
17
|
+
f"Invalid spec: {spec} (deprecated; use core.all/device.all/core.<file>/device.<file>)"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if spec == "core.all":
|
|
21
|
+
return "core", ""
|
|
22
|
+
if spec == "device.all":
|
|
23
|
+
return "device", ""
|
|
24
|
+
|
|
25
|
+
if spec.startswith("core."):
|
|
26
|
+
rest = spec[len("core."):]
|
|
27
|
+
if not rest:
|
|
28
|
+
raise PkgError("Invalid spec: core.<file> requires a filename")
|
|
29
|
+
if rest.count(".") > 1:
|
|
30
|
+
raise PkgError(f"Invalid spec: {spec} (nested dotted filename is not supported)")
|
|
31
|
+
return "core", rest
|
|
32
|
+
|
|
33
|
+
if spec.startswith("device."):
|
|
34
|
+
rest = spec[len("device."):]
|
|
35
|
+
if not rest:
|
|
36
|
+
raise PkgError("Invalid spec: device.<file> requires a filename")
|
|
37
|
+
if rest.count(".") > 1:
|
|
38
|
+
raise PkgError(f"Invalid spec: {spec} (nested dotted filename is not supported)")
|
|
39
|
+
return "device", rest
|
|
40
|
+
|
|
41
|
+
raise PkgError(
|
|
42
|
+
f"Invalid spec: {spec} (expect core.all/device.all/core.<file>/device.<file>)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def download_raw_file(owner: str, repo: str, ref_: str, path: str, out_path: str) -> str:
|
|
47
|
+
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref_}/{path}"
|
|
48
|
+
req = urllib.request.Request(url, headers=StoreManager.gh_headers())
|
|
49
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
50
|
+
with urllib.request.urlopen(req, timeout=HTTP_REQUEST_TIMEOUT) as r, open(out_path, "wb") as f:
|
|
51
|
+
f.write(r.read())
|
|
52
|
+
return out_path
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def remote_dir_for(scope: str, rel_dir: str, device: str) -> str:
|
|
56
|
+
if scope == "core":
|
|
57
|
+
return "lib/" + (rel_dir + "/" if rel_dir else "")
|
|
58
|
+
return f"lib/{device_name_to_path(device)}/" + (rel_dir + "/" if rel_dir else "")
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def list_local_py_targets(scope: str, rest: str, core: str, device: str) -> Tuple[str, list]:
|
|
62
|
+
if scope == "core":
|
|
63
|
+
base = os.path.join(StoreManager.pkg_root(), "core", core, "src")
|
|
64
|
+
else:
|
|
65
|
+
base = os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "src")
|
|
66
|
+
|
|
67
|
+
target_path = os.path.join(base, rest)
|
|
68
|
+
if os.path.isfile(target_path):
|
|
69
|
+
rel = os.path.relpath(target_path, base).replace("\\", "/")
|
|
70
|
+
return base, [(target_path, rel)]
|
|
71
|
+
|
|
72
|
+
if os.path.isdir(target_path):
|
|
73
|
+
out = []
|
|
74
|
+
for dp, _, fns in os.walk(target_path):
|
|
75
|
+
for fn in fns:
|
|
76
|
+
ap = os.path.join(dp, fn)
|
|
77
|
+
rel = os.path.relpath(ap, base).replace("\\", "/")
|
|
78
|
+
out.append((ap, rel))
|
|
79
|
+
return base, out
|
|
80
|
+
return base, []
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def local_store_ready_for_full_install(core: str, device: str) -> Tuple[bool, str]:
|
|
84
|
+
StoreManager.ensure_home_store()
|
|
85
|
+
meta_path = StoreManager.local_meta_path()
|
|
86
|
+
if not os.path.isfile(meta_path):
|
|
87
|
+
return False, "meta-missing"
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
_ = StoreManager.load_local_meta()
|
|
91
|
+
except Exception:
|
|
92
|
+
return False, "meta-broken"
|
|
93
|
+
|
|
94
|
+
req_dirs = [
|
|
95
|
+
os.path.join(StoreManager.pkg_root(), "core", core, "src"),
|
|
96
|
+
os.path.join(StoreManager.pkg_root(), "core", core, "typehints"),
|
|
97
|
+
os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "src"),
|
|
98
|
+
os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "typehints"),
|
|
99
|
+
]
|
|
100
|
+
missing = [p for p in req_dirs if not os.path.isdir(p)]
|
|
101
|
+
if missing:
|
|
102
|
+
return False, "dirs-missing"
|
|
103
|
+
|
|
104
|
+
return True, "ok"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SearchHelper:
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def fmt_ver_with_star(remote_ver: float, local_ver: float, missing_local: bool) -> str:
|
|
111
|
+
star = "*" if missing_local or (remote_ver > (local_ver or 0.0)) else ""
|
|
112
|
+
return f"{remote_ver:.1f}{star}"
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def key_ci(items: set | dict, name: str) -> Optional[str]:
|
|
116
|
+
if not name:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
if isinstance(items, set):
|
|
120
|
+
if name in items:
|
|
121
|
+
return name
|
|
122
|
+
n = name.lower()
|
|
123
|
+
for k in items:
|
|
124
|
+
if isinstance(k, str) and k.lower() == n:
|
|
125
|
+
return k
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
if isinstance(items, dict):
|
|
129
|
+
if name in items:
|
|
130
|
+
return name
|
|
131
|
+
n = name.lower()
|
|
132
|
+
for k in items.keys():
|
|
133
|
+
if isinstance(k, str) and k.lower() == n:
|
|
134
|
+
return k
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class RegistryHelper:
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def root_sections(reg: dict):
|
|
144
|
+
platform_cores = reg.get("platform_cores", {})
|
|
145
|
+
device_configs = reg.get("device_configs", {})
|
|
146
|
+
|
|
147
|
+
cores = set(platform_cores.keys()) if platform_cores else set()
|
|
148
|
+
devices = set(device_configs.keys()) if device_configs else set()
|
|
149
|
+
|
|
150
|
+
return cores, devices
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def get_packages_for_platform(reg: dict, platform: str) -> list[str]:
|
|
154
|
+
platform_cores = reg.get("platform_cores", {})
|
|
155
|
+
core_info = platform_cores.get(platform, {})
|
|
156
|
+
return core_info.get("includes", [])
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def get_packages_for_device(reg: dict, device: str) -> list[str]:
|
|
160
|
+
device_configs = reg.get("device_configs", {})
|
|
161
|
+
device_info = device_configs.get(device, {})
|
|
162
|
+
return device_info.get("includes", [])
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def match_pattern(pkg_name: str, pattern: str) -> bool:
|
|
166
|
+
if "*" in pattern:
|
|
167
|
+
# Wildcard pattern
|
|
168
|
+
prefix = pattern.rstrip("/*")
|
|
169
|
+
return pkg_name.startswith(prefix)
|
|
170
|
+
else:
|
|
171
|
+
pkg_base = pkg_name
|
|
172
|
+
for ext in (".py", ".pyi", ".json"):
|
|
173
|
+
if pkg_base.endswith(ext):
|
|
174
|
+
pkg_base = pkg_base[:-len(ext)]
|
|
175
|
+
break
|
|
176
|
+
return pkg_base == pattern or pkg_name == pattern
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def get_packages_matching(reg: dict, patterns: list[str], package_type: str = None) -> dict:
|
|
180
|
+
packages = reg.get("packages", {})
|
|
181
|
+
result = {}
|
|
182
|
+
|
|
183
|
+
for pkg_name, pkg_meta in packages.items():
|
|
184
|
+
if package_type:
|
|
185
|
+
pkg_type = pkg_meta.get("type", "")
|
|
186
|
+
if package_type == "core" and pkg_type != "core":
|
|
187
|
+
continue
|
|
188
|
+
elif package_type == "device" and not pkg_type.startswith("device-"):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
source = pkg_meta.get("source", "")
|
|
192
|
+
for pattern in patterns:
|
|
193
|
+
if RegistryHelper.match_pattern(source, pattern):
|
|
194
|
+
result[pkg_name] = pkg_meta
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def get_version(pkg_meta: dict) -> float:
|
|
201
|
+
v = pkg_meta.get("version", "0.0")
|
|
202
|
+
try:
|
|
203
|
+
if isinstance(v, str) and "." in v:
|
|
204
|
+
parts = v.split(".")
|
|
205
|
+
major = int(parts[0]) if len(parts) > 0 and parts[0].isdigit() else 0
|
|
206
|
+
minor = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0
|
|
207
|
+
return float(f"{major}.{minor}")
|
|
208
|
+
return float(v)
|
|
209
|
+
except Exception:
|
|
210
|
+
return 0.0
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def effective_version(reg: dict, scope: str, target: str, part: str, relpath: str) -> float:
|
|
214
|
+
packages = reg.get("packages", {})
|
|
215
|
+
|
|
216
|
+
if scope == "core":
|
|
217
|
+
source_path = f"core/{target}/{part}/{relpath}"
|
|
218
|
+
else:
|
|
219
|
+
source_path = f"device/{target}/{part}/{relpath}"
|
|
220
|
+
|
|
221
|
+
for pkg_name, pkg_meta in packages.items():
|
|
222
|
+
pkg_source = pkg_meta.get("source", "")
|
|
223
|
+
if pkg_source == source_path:
|
|
224
|
+
return RegistryHelper.get_version(pkg_meta)
|
|
225
|
+
|
|
226
|
+
return 0.0
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def walk_files_for_core(reg: dict, core_name: str, part: str = "src"):
|
|
230
|
+
packages = reg.get("packages", {})
|
|
231
|
+
patterns = RegistryHelper.get_packages_for_platform(reg, core_name)
|
|
232
|
+
|
|
233
|
+
for pkg_name, pkg_meta in packages.items():
|
|
234
|
+
if pkg_meta.get("type") != "core":
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
source = pkg_meta.get("source", "")
|
|
238
|
+
|
|
239
|
+
match_found = False
|
|
240
|
+
for pattern in patterns:
|
|
241
|
+
if "*" in pattern:
|
|
242
|
+
pattern_prefix = pattern.rstrip("/*")
|
|
243
|
+
if f"/{pattern_prefix}/" in source or source.endswith(f"/{pattern_prefix}"):
|
|
244
|
+
match_found = True
|
|
245
|
+
break
|
|
246
|
+
else:
|
|
247
|
+
if RegistryHelper.match_pattern(source, pattern):
|
|
248
|
+
match_found = True
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
if not match_found:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if part == "src":
|
|
255
|
+
file_path = source
|
|
256
|
+
elif part == "typehints":
|
|
257
|
+
file_path = pkg_meta.get("typehint", "")
|
|
258
|
+
if not file_path:
|
|
259
|
+
continue
|
|
260
|
+
else:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
path_parts = file_path.split("/")
|
|
264
|
+
if len(path_parts) >= 3 and path_parts[2] == part:
|
|
265
|
+
relpath = "/".join(path_parts[3:])
|
|
266
|
+
yield (relpath, pkg_meta)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def walk_files_for_device(reg: dict, device_name: str, part: str = "src", include_submodules: bool = False):
|
|
270
|
+
packages = reg.get("packages", {})
|
|
271
|
+
patterns = RegistryHelper.get_packages_for_device(reg, device_name)
|
|
272
|
+
|
|
273
|
+
for pkg_name, pkg_meta in packages.items():
|
|
274
|
+
variants = pkg_meta.get("variants", {})
|
|
275
|
+
for variant_name, variant_meta in variants.items():
|
|
276
|
+
variant_deploy = variant_meta.get("deploy_path", "")
|
|
277
|
+
|
|
278
|
+
match_found = False
|
|
279
|
+
for pattern in patterns:
|
|
280
|
+
module_name = variant_deploy.replace("/__init__.py", "").replace(".py", "")
|
|
281
|
+
variant_full_path = f"{variant_name}/{module_name}"
|
|
282
|
+
|
|
283
|
+
if RegistryHelper.match_pattern(variant_full_path, pattern):
|
|
284
|
+
match_found = True
|
|
285
|
+
break
|
|
286
|
+
if "/" in pattern and variant_full_path.startswith(pattern + "/"):
|
|
287
|
+
match_found = True
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not match_found:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if part == "src":
|
|
294
|
+
file_path = variant_meta.get("source", "")
|
|
295
|
+
elif part == "typehints":
|
|
296
|
+
file_path = variant_meta.get("typehint", "")
|
|
297
|
+
if not file_path or not file_path.endswith(".pyi"):
|
|
298
|
+
continue
|
|
299
|
+
else:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
if not file_path:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
if part == "typehints":
|
|
306
|
+
relpath = variant_deploy.replace(".py", ".pyi")
|
|
307
|
+
else:
|
|
308
|
+
relpath = variant_deploy
|
|
309
|
+
merged_meta = pkg_meta.copy()
|
|
310
|
+
merged_meta.update(variant_meta)
|
|
311
|
+
yield (relpath, merged_meta)
|
|
312
|
+
|
|
313
|
+
pkg_type = pkg_meta.get("type", "")
|
|
314
|
+
if not pkg_type.startswith("device-"):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
source = pkg_meta.get("source", "")
|
|
318
|
+
deploy_path = pkg_meta.get("deploy_path", "")
|
|
319
|
+
|
|
320
|
+
match_found = False
|
|
321
|
+
for pattern in patterns:
|
|
322
|
+
if "@" in pkg_name:
|
|
323
|
+
parts = pkg_name.split("@")
|
|
324
|
+
module_name = parts[0]
|
|
325
|
+
device_prefix = parts[1]
|
|
326
|
+
variant_full_path = f"{device_prefix}/{module_name}"
|
|
327
|
+
if RegistryHelper.match_pattern(variant_full_path, pattern):
|
|
328
|
+
match_found = True
|
|
329
|
+
break
|
|
330
|
+
|
|
331
|
+
if source.startswith("device/"):
|
|
332
|
+
source_parts = source.split("/")
|
|
333
|
+
if len(source_parts) >= 4:
|
|
334
|
+
category = source_parts[1]
|
|
335
|
+
file_parts = source_parts[3:]
|
|
336
|
+
|
|
337
|
+
file_path = "/".join(file_parts)
|
|
338
|
+
if file_path.endswith("/__init__.py"):
|
|
339
|
+
file_path = file_path[:-12]
|
|
340
|
+
elif file_path.endswith(".py") or file_path.endswith(".pyi"):
|
|
341
|
+
file_path = file_path[:-3]
|
|
342
|
+
|
|
343
|
+
source_pattern_path = f"{category}/{file_path}"
|
|
344
|
+
|
|
345
|
+
if RegistryHelper.match_pattern(source_pattern_path, pattern):
|
|
346
|
+
match_found = True
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
if RegistryHelper.match_pattern(deploy_path, pattern):
|
|
350
|
+
match_found = True
|
|
351
|
+
break
|
|
352
|
+
if "/" in pattern and deploy_path.startswith(pattern + "/"):
|
|
353
|
+
match_found = True
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
if not match_found:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
if part == "src":
|
|
360
|
+
file_path = source
|
|
361
|
+
elif part == "typehints":
|
|
362
|
+
file_path = pkg_meta.get("typehint", "")
|
|
363
|
+
if not file_path or not file_path.endswith(".pyi"):
|
|
364
|
+
continue
|
|
365
|
+
else:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
if part == "typehints":
|
|
369
|
+
relpath = deploy_path.replace(".py", ".pyi")
|
|
370
|
+
else:
|
|
371
|
+
relpath = deploy_path
|
|
372
|
+
yield (relpath, pkg_meta)
|
|
373
|
+
|
|
374
|
+
if include_submodules and part == "src":
|
|
375
|
+
deploy_dir = "/".join(deploy_path.split("/")[:-1])
|
|
376
|
+
sub_base = {k: v for k, v in pkg_meta.items() if k not in ("submodules", "submodules_typehints")}
|
|
377
|
+
sub_base["submodules"] = []
|
|
378
|
+
sub_base["submodules_typehints"] = []
|
|
379
|
+
sub_base["_parent_source"] = pkg_meta.get("source", "")
|
|
380
|
+
for sub_src in pkg_meta.get("submodules", []):
|
|
381
|
+
sub_filename = sub_src.split("/")[-1]
|
|
382
|
+
if sub_filename.endswith(".pyi"):
|
|
383
|
+
continue
|
|
384
|
+
sub_relpath = (deploy_dir + "/" + sub_filename) if deploy_dir else sub_filename
|
|
385
|
+
sub_meta = sub_base.copy()
|
|
386
|
+
sub_meta["source"] = sub_src
|
|
387
|
+
yield (sub_relpath, sub_meta)
|