ErisPulse 2.1.11__py3-none-any.whl → 2.1.13__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.
ErisPulse/__main__.py CHANGED
@@ -1,3 +1,14 @@
1
+ """
2
+ ErisPulse SDK 命令行工具
3
+
4
+ 提供ErisPulse生态系统的包管理、模块控制和开发工具功能。
5
+
6
+ {!--< tips >!--}
7
+ 1. 需要Python 3.8+环境
8
+ 2. Windows平台需要colorama支持ANSI颜色
9
+ {!--< /tips >!--}
10
+ """
11
+
1
12
  import argparse
2
13
  import importlib.metadata
3
14
  import subprocess
@@ -7,7 +18,7 @@ import time
7
18
  import json
8
19
  import asyncio
9
20
  from urllib.parse import urlparse
10
- from typing import List, Dict, Tuple, Optional
21
+ from typing import List, Dict, Tuple, Optional, Callable, Any
11
22
  from importlib.metadata import version, PackageNotFoundError
12
23
  from watchdog.observers import Observer
13
24
  from watchdog.events import FileSystemEventHandler
@@ -24,114 +35,159 @@ from rich.style import Style
24
35
  from rich.theme import Theme
25
36
  from rich.layout import Layout
26
37
  from rich.live import Live
38
+ from rich.markdown import Markdown
39
+ from rich.highlighter import RegexHighlighter
27
40
 
28
41
  # 确保在Windows上启用颜色
29
- import sys
30
42
  if sys.platform == "win32":
31
43
  from colorama import init
32
44
  init()
33
45
 
46
+ class CommandHighlighter(RegexHighlighter):
47
+ """
48
+ 高亮CLI命令和参数
49
+
50
+ {!--< tips >!--}
51
+ 使用正则表达式匹配命令行参数和选项
52
+ {!--< /tips >!--}
53
+ """
54
+ highlights = [
55
+ r"(?P<switch>\-\-?\w+)",
56
+ r"(?P<option>\[\w+\])",
57
+ r"(?P<command>\b\w+\b)",
58
+ ]
59
+
60
+ # 主题配置
34
61
  theme = Theme({
35
- "info": "cyan",
36
- "success": "green",
37
- "warning": "yellow",
38
- "error": "red",
39
- "title": "magenta",
62
+ "info": "dim cyan",
63
+ "success": "bold green",
64
+ "warning": "bold yellow",
65
+ "error": "bold red",
66
+ "title": "bold magenta",
40
67
  "default": "default",
41
68
  "progress": "green",
42
69
  "progress.remaining": "white",
70
+ "cmd": "bold blue",
71
+ "param": "italic cyan",
72
+ "switch": "bold yellow",
73
+ "module": "bold green",
74
+ "adapter": "bold yellow",
75
+ "cli": "bold magenta",
43
76
  })
44
77
 
45
- console = Console(theme=theme, color_system="auto", force_terminal=True)
78
+ # 全局控制台实例
79
+ console = Console(
80
+ theme=theme,
81
+ color_system="auto",
82
+ force_terminal=True,
83
+ highlighter=CommandHighlighter()
84
+ )
46
85
 
47
- class PyPIManager:
86
+ class PackageManager:
48
87
  """
49
- PyPI包管理器
88
+ ErisPulse包管理器
50
89
 
51
- 负责与PyPI交互,包括搜索、安装、卸载和升级ErisPulse模块/适配器
90
+ 提供包安装、卸载、升级和查询功能
91
+
92
+ {!--< tips >!--}
93
+ 1. 支持本地和远程包管理
94
+ 2. 包含1小时缓存机制
95
+ {!--< /tips >!--}
52
96
  """
53
97
  REMOTE_SOURCES = [
54
98
  "https://erisdev.com/packages.json",
55
99
  "https://raw.githubusercontent.com/ErisPulse/ErisPulse-ModuleRepo/main/packages.json"
56
100
  ]
57
101
 
58
- @staticmethod
59
- async def get_remote_packages() -> dict:
102
+ CACHE_EXPIRY = 3600 # 1小时缓存
103
+
104
+ def __init__(self):
105
+ """初始化包管理器"""
106
+ self._cache = {}
107
+ self._cache_time = {}
108
+
109
+ async def _fetch_remote_packages(self, url: str) -> Optional[dict]:
60
110
  """
61
- 获取远程包列表
111
+ 从指定URL获取远程包数据
62
112
 
63
- 从配置的远程源获取所有可用的ErisPulse模块和适配器
64
-
65
- :return:
66
- Dict[str, Dict]: 包含模块和适配器的字典
67
- - modules: 模块字典 {模块名: 模块信息}
68
- - adapters: 适配器字典 {适配器名: 适配器信息}
69
-
70
- :raises ClientError: 当网络请求失败时抛出
71
- :raises asyncio.TimeoutError: 当请求超时时抛出
113
+ :param url: 远程包数据URL
114
+ :return: 解析后的JSON数据,失败返回None
115
+
116
+ :raises ClientError: 网络请求失败时抛出
117
+ :raises JSONDecodeError: JSON解析失败时抛出
72
118
  """
73
119
  import aiohttp
74
120
  from aiohttp import ClientError, ClientTimeout
75
121
 
76
- timeout = ClientTimeout(total=5)
77
- last_error = None
78
-
122
+ timeout = ClientTimeout(total=10)
79
123
  try:
80
124
  async with aiohttp.ClientSession(timeout=timeout) as session:
81
- async with session.get(PyPIManager.REMOTE_SOURCES[0]) as response:
125
+ async with session.get(url) as response:
82
126
  if response.status == 200:
83
127
  data = await response.text()
84
- data = json.loads(data)
85
- return {
86
- "modules": data.get("modules", {}),
87
- "adapters": data.get("adapters", {})
88
- }
89
- except (ClientError, asyncio.TimeoutError) as e:
90
- last_error = e
91
- console.print(Panel(
92
- f"官方源请求失败,尝试备用源: {e}",
93
- title="警告",
94
- style="warning"
95
- ))
128
+ return json.loads(data)
129
+ except (ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
130
+ console.print(f"[warning]获取远程包数据失败 ({url}): {e}[/]")
131
+ return None
132
+
133
+ async def get_remote_packages(self, force_refresh: bool = False) -> dict:
134
+ """
135
+ 获取远程包列表,带缓存机制
96
136
 
97
- try:
98
- async with aiohttp.ClientSession(timeout=timeout) as session:
99
- async with session.get(PyPIManager.REMOTE_SOURCES[1]) as response:
100
- if response.status == 200:
101
- data = await response.text()
102
- data = json.loads(data)
103
- return {
104
- "modules": data.get("modules", {}),
105
- "adapters": data.get("adapters", {})
106
- }
107
- except (ClientError, asyncio.TimeoutError) as e:
108
- last_error = e
109
-
110
- if last_error:
111
- console.print(Panel(
112
- f"获取远程模块列表失败: {last_error}",
113
- title="错误",
114
- style="error"
115
- ))
116
- return {"modules": {}, "adapters": {}}
137
+ :param force_refresh: 是否强制刷新缓存
138
+ :return: 包含模块和适配器的字典
139
+
140
+ :return:
141
+ dict: {
142
+ "modules": {模块名: 模块信息},
143
+ "adapters": {适配器名: 适配器信息},
144
+ "cli_extensions": {扩展名: 扩展信息}
145
+ }
146
+ """
147
+ # 检查缓存
148
+ cache_key = "remote_packages"
149
+ if not force_refresh and cache_key in self._cache:
150
+ if time.time() - self._cache_time[cache_key] < self.CACHE_EXPIRY:
151
+ return self._cache[cache_key]
152
+
153
+ last_error = None
154
+ result = {"modules": {}, "adapters": {}, "cli_extensions": {}}
155
+
156
+ for url in self.REMOTE_SOURCES:
157
+ data = await self._fetch_remote_packages(url)
158
+ if data:
159
+ result["modules"].update(data.get("modules", {}))
160
+ result["adapters"].update(data.get("adapters", {}))
161
+ result["cli_extensions"].update(data.get("cli_extensions", {}))
162
+ break
163
+
164
+ # 更新缓存
165
+ self._cache[cache_key] = result
166
+ self._cache_time[cache_key] = time.time()
167
+
168
+ return result
117
169
 
118
- @staticmethod
119
- def get_installed_packages() -> Dict[str, Dict[str, str]]:
170
+ def get_installed_packages(self) -> Dict[str, Dict[str, Dict[str, str]]]:
120
171
  """
121
172
  获取已安装的包信息
122
173
 
123
- :return:
124
- Dict[str, Dict[str, Dict[str, str]]]: 已安装包字典
125
- - modules: 已安装模块 {模块名: 模块信息}
126
- - adapters: 已安装适配器 {适配器名: 适配器信息}
174
+ :return: 已安装包字典,包含模块、适配器和CLI扩展
175
+
176
+ :return:
177
+ dict: {
178
+ "modules": {模块名: 模块信息},
179
+ "adapters": {适配器名: 适配器信息},
180
+ "cli_extensions": {扩展名: 扩展信息}
181
+ }
127
182
  """
128
183
  packages = {
129
184
  "modules": {},
130
- "adapters": {}
185
+ "adapters": {},
186
+ "cli_extensions": {}
131
187
  }
132
188
 
133
189
  try:
134
- # 查找模块
190
+ # 查找模块和适配器
135
191
  for dist in importlib.metadata.distributions():
136
192
  if "ErisPulse-" in dist.metadata["Name"]:
137
193
  entry_points = dist.entry_points
@@ -140,7 +196,8 @@ class PyPIManager:
140
196
  packages["modules"][ep.name] = {
141
197
  "package": dist.metadata["Name"],
142
198
  "version": dist.version,
143
- "summary": dist.metadata["Summary"]
199
+ "summary": dist.metadata["Summary"],
200
+ "enabled": self._is_module_enabled(ep.name)
144
201
  }
145
202
  elif ep.group == "erispulse.adapter":
146
203
  packages["adapters"][ep.name] = {
@@ -148,530 +205,889 @@ class PyPIManager:
148
205
  "version": dist.version,
149
206
  "summary": dist.metadata["Summary"]
150
207
  }
208
+
209
+ # 查找CLI扩展
210
+ entry_points = importlib.metadata.entry_points()
211
+ if hasattr(entry_points, 'select'):
212
+ cli_entries = entry_points.select(group='erispulse.cli')
213
+ else:
214
+ cli_entries = entry_points.get('erispulse.cli', [])
215
+
216
+ for entry in cli_entries:
217
+ dist = entry.dist
218
+ packages["cli_extensions"][entry.name] = {
219
+ "package": dist.metadata["Name"],
220
+ "version": dist.version,
221
+ "summary": dist.metadata["Summary"]
222
+ }
223
+
151
224
  except Exception as e:
152
- console.print(Panel(
153
- f"获取已安装包信息失败: {e}",
154
- title="错误",
155
- style="error"
156
- ))
225
+ console.print(f"[error]获取已安装包信息失败: {e}[/]")
226
+ import traceback
227
+ console.print(traceback.format_exc())
157
228
 
158
229
  return packages
159
230
 
160
- @staticmethod
161
- def uv_install_package(package_name: str, upgrade: bool = False) -> bool:
231
+ def _is_module_enabled(self, module_name: str) -> bool:
162
232
  """
163
- 优先使用uv安装包
233
+ 检查模块是否启用
234
+
235
+ :param module_name: 模块名称
236
+ :return: 模块是否启用
164
237
 
165
- :param package_name: str 要安装的包名
166
- :param upgrade: bool 是否升级已安装的包 (默认: False)
167
- :return: bool 安装是否成功
238
+ :raises ImportError: 核心模块不可用时抛出
168
239
  """
169
240
  try:
170
- # 检查uv是否可用
171
- uv_check = subprocess.run(["uv", "--version"], capture_output=True)
172
- if uv_check.returncode != 0:
173
- return False # uv不可用
174
-
175
- cmd = ["uv", "pip", "install"]
176
- if upgrade:
177
- cmd.append("--upgrade")
178
- cmd.append(package_name)
179
-
180
- with console.status(f"使用uv安装 {package_name}..."):
181
- result = subprocess.run(cmd, check=True)
182
- return result.returncode == 0
183
- except (subprocess.CalledProcessError, FileNotFoundError):
241
+ from ErisPulse.Core import mods
242
+ return mods.get_module_status(module_name)
243
+ except ImportError:
244
+ return True
245
+ except Exception:
184
246
  return False
185
247
 
186
- @staticmethod
187
- def install_package(package_name: str, upgrade: bool = False) -> bool:
248
+ def _run_pip_command(self, args: List[str], description: str) -> bool:
188
249
  """
189
- 安装指定包 (修改后优先尝试uv)
250
+ 执行pip命令
190
251
 
191
- :param package_name: str 要安装的包名
192
- :param upgrade: bool 是否升级已安装的包 (默认: False)
193
- :return: bool 安装是否成功
252
+ :param args: pip命令参数列表
253
+ :param description: 进度条描述
254
+ :return: 命令是否成功执行
194
255
  """
195
- # 优先尝试uv安装
196
- if PyPIManager.uv_install_package(package_name, upgrade):
197
- console.print(Panel(
198
- f"包 {package_name} 安装成功 (使用uv)",
199
- title="成功",
200
- style="success"
201
- ))
202
- return True
256
+ with Progress(
257
+ TextColumn(f"[progress.description]{description}"),
258
+ BarColumn(complete_style="progress.download"),
259
+ transient=True
260
+ ) as progress:
261
+ task = progress.add_task("", total=100)
203
262
 
204
- # 回退到pip安装
205
- try:
206
- cmd = [sys.executable, "-m", "pip", "install"]
207
- if upgrade:
208
- cmd.append("--upgrade")
209
- cmd.append(package_name)
210
-
211
- with console.status(f"使用pip安装 {package_name}..."):
212
- result = subprocess.run(cmd, check=True)
213
- if result.returncode == 0:
214
- console.print(Panel(
215
- f"包 {package_name} 安装成功",
216
- title="成功",
217
- style="success"
218
- ))
219
- return True
263
+ try:
264
+ process = subprocess.Popen(
265
+ [sys.executable, "-m", "pip"] + args,
266
+ stdout=subprocess.PIPE,
267
+ stderr=subprocess.PIPE,
268
+ universal_newlines=True
269
+ )
270
+
271
+ while True:
272
+ output = process.stdout.readline()
273
+ if output == '' and process.poll() is not None:
274
+ break
275
+ if output:
276
+ progress.update(task, advance=1)
277
+
278
+ return process.returncode == 0
279
+ except subprocess.CalledProcessError as e:
280
+ console.print(f"[error]命令执行失败: {e}[/]")
220
281
  return False
221
- except subprocess.CalledProcessError as e:
222
- console.print(Panel(
223
- f"安装包 {package_name} 失败: {e}",
224
- title="错误",
225
- style="error"
226
- ))
227
- return False
228
282
 
229
- @staticmethod
230
- def uninstall_package(package_name: str) -> bool:
283
+ def install_package(self, package_name: str, upgrade: bool = False) -> bool:
284
+ """
285
+ 安装指定包
286
+
287
+ :param package_name: 要安装的包名
288
+ :param upgrade: 是否升级已安装的包
289
+ :return: 安装是否成功
290
+ """
291
+ cmd = ["install"]
292
+ if upgrade:
293
+ cmd.append("--upgrade")
294
+ cmd.append(package_name)
295
+
296
+ success = self._run_pip_command(cmd, f"安装 {package_name}")
297
+
298
+ if success:
299
+ console.print(f"[success]包 {package_name} 安装成功[/]")
300
+ else:
301
+ console.print(f"[error]包 {package_name} 安装失败[/]")
302
+
303
+ return success
304
+
305
+ def uninstall_package(self, package_name: str) -> bool:
231
306
  """
232
307
  卸载指定包
233
308
 
234
- :param package_name: str 要卸载的包名
235
- :return: bool 卸载是否成功
309
+ :param package_name: 要卸载的包名
310
+ :return: 卸载是否成功
236
311
  """
237
- try:
238
- with console.status(f"正在卸载 {package_name}..."):
239
- result = subprocess.run(
240
- [sys.executable, "-m", "pip", "uninstall", "-y", package_name],
241
- check=True
242
- )
243
- if result.returncode == 0:
244
- console.print(Panel(
245
- f"包 {package_name} 卸载成功",
246
- title="成功",
247
- style="success"
248
- ))
249
- return True
250
- return False
251
- except subprocess.CalledProcessError as e:
252
- console.print(Panel(
253
- f"卸载包 {package_name} 失败: {e}",
254
- title="错误",
255
- style="error"
256
- ))
257
- return False
312
+ success = self._run_pip_command(
313
+ ["uninstall", "-y", package_name],
314
+ f"卸载 {package_name}"
315
+ )
316
+
317
+ if success:
318
+ console.print(f"[success]包 {package_name} 卸载成功[/]")
319
+ else:
320
+ console.print(f"[error]包 {package_name} 卸载失败[/]")
321
+
322
+ return success
258
323
 
259
- @staticmethod
260
- def upgrade_all() -> bool:
324
+ def upgrade_all(self) -> bool:
261
325
  """
262
326
  升级所有已安装的ErisPulse包
263
327
 
264
- :return: bool 升级是否成功
328
+ :return: 升级是否成功
329
+
330
+ :raises KeyboardInterrupt: 用户取消操作时抛出
265
331
  """
266
- try:
267
- installed = PyPIManager.get_installed_packages()
268
- all_packages = set()
269
-
270
- for pkg_type in ["modules", "adapters"]:
271
- for pkg_info in installed[pkg_type].values():
272
- all_packages.add(pkg_info["package"])
273
-
274
- if not all_packages:
275
- console.print(Panel(
276
- "没有找到可升级的ErisPulse包",
277
- title="提示",
278
- style="info"
279
- ))
280
- return False
281
-
282
- console.print(Panel(
283
- f"找到 {len(all_packages)} 个可升级的包:\n" +
284
- "\n".join(f" - {pkg}" for pkg in all_packages),
285
- title="升级列表",
286
- style="info"
287
- ))
332
+ installed = self.get_installed_packages()
333
+ all_packages = set()
334
+
335
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
336
+ for pkg_info in installed[pkg_type].values():
337
+ all_packages.add(pkg_info["package"])
338
+
339
+ if not all_packages:
340
+ console.print("[info]没有找到可升级的ErisPulse包[/]")
341
+ return False
288
342
 
289
- if not Confirm.ask("确认升级所有包吗?", default=False):
290
- return False
291
-
292
- for pkg in all_packages:
293
- PyPIManager.install_package(pkg, upgrade=True)
294
-
295
- return True
296
- except Exception as e:
343
+ console.print(Panel(
344
+ f"找到 [bold]{len(all_packages)}[/] 个可升级的包:\n" +
345
+ "\n".join(f" - [package]{pkg}[/]" for pkg in sorted(all_packages)),
346
+ title="升级列表"
347
+ ))
348
+
349
+ if not Confirm.ask("确认升级所有包吗?", default=False):
350
+ return False
351
+
352
+ results = {}
353
+ for pkg in sorted(all_packages):
354
+ results[pkg] = self.install_package(pkg, upgrade=True)
355
+
356
+ failed = [pkg for pkg, success in results.items() if not success]
357
+ if failed:
297
358
  console.print(Panel(
298
- f"升级包失败: {e}",
299
- title="错误",
300
- style="error"
359
+ f"以下包升级失败:\n" + "\n".join(f" - [error]{pkg}[/]" for pkg in failed),
360
+ title="警告",
361
+ style="warning"
301
362
  ))
302
363
  return False
364
+
365
+ return True
303
366
 
304
367
  class ReloadHandler(FileSystemEventHandler):
305
368
  """
306
- 热重载处理器
369
+ 文件系统事件处理器
370
+
371
+ 实现热重载功能,监控文件变化并重启进程
307
372
 
308
- 监控文件变化并自动重启脚本
373
+ {!--< tips >!--}
374
+ 1. 支持.py文件修改重载
375
+ 2. 支持配置文件修改重载
376
+ {!--< /tips >!--}
309
377
  """
310
- def __init__(self, script_path, reload_mode=False, *args, **kwargs):
311
- super().__init__(*args, **kwargs)
312
- self.script_path = script_path
378
+
379
+ def __init__(self, script_path: str, reload_mode: bool = False):
380
+ """
381
+ 初始化处理器
382
+
383
+ :param script_path: 要监控的脚本路径
384
+ :param reload_mode: 是否启用重载模式
385
+ """
386
+ super().__init__()
387
+ self.script_path = os.path.abspath(script_path)
313
388
  self.process = None
314
389
  self.last_reload = time.time()
315
390
  self.reload_mode = reload_mode
316
391
  self.start_process()
392
+ self.watched_files = set()
317
393
 
318
394
  def start_process(self):
395
+ """启动监控进程"""
396
+ if self.process:
397
+ self._terminate_process()
398
+
399
+ console.print(f"[bold]启动进程: [path]{self.script_path}[/][/]")
400
+ try:
401
+ self.process = subprocess.Popen(
402
+ [sys.executable, self.script_path],
403
+ stdin=sys.stdin,
404
+ stdout=sys.stdout,
405
+ stderr=sys.stderr
406
+ )
407
+ self.last_reload = time.time()
408
+ except Exception as e:
409
+ console.print(f"[error]启动进程失败: {e}[/]")
410
+ raise
411
+
412
+ def _terminate_process(self):
319
413
  """
320
- 启动/重启被监控的进程
414
+ 终止当前进程
415
+
416
+ :raises subprocess.TimeoutExpired: 进程终止超时时抛出
321
417
  """
322
- if self.process:
418
+ try:
323
419
  self.process.terminate()
420
+ # 等待最多2秒让进程正常退出
421
+ self.process.wait(timeout=2)
422
+ except subprocess.TimeoutExpired:
423
+ console.print("[warning]进程未正常退出,强制终止...[/]")
424
+ self.process.kill()
324
425
  self.process.wait()
325
-
326
- console.print(f"[bold green]启动进程:[/] {self.script_path}")
327
- self.process = subprocess.Popen([sys.executable, self.script_path])
328
- self.last_reload = time.time()
426
+ except Exception as e:
427
+ console.print(f"[error]终止进程时出错: {e}[/]")
329
428
 
330
429
  def on_modified(self, event):
331
430
  """
332
431
  文件修改事件处理
333
432
 
334
- :param event: FileSystemEvent 文件系统事件对象
433
+ :param event: 文件系统事件
335
434
  """
336
435
  now = time.time()
337
- if now - self.last_reload < 1.0:
436
+ if now - self.last_reload < 1.0: # 防抖
338
437
  return
339
438
 
340
439
  if event.src_path.endswith(".py") and self.reload_mode:
341
- console.print(f"\n[cyan][热重载] 文件发生变动: {event.src_path}[/]")
342
- self.start_process()
343
- elif event.src_path.endswith("config.toml"):
344
- console.print(f"\n[cyan][热重载] 配置发生变动: {event.src_path}[/]")
345
- self.start_process()
346
-
347
- def start_reloader(script_path, reload_mode=False):
348
- """
349
- 启动热重载监控
350
-
351
- :param script_path: str 要监控的脚本路径
352
- :param reload_mode: bool 是否启用完整重载模式 (默认: False)
353
- """
354
- if not os.path.exists(script_path):
355
- console.print(Panel(
356
- f"找不到指定文件: {script_path}",
357
- title="错误",
358
- style="error"
359
- ))
360
- return
361
- watch_dirs = [
362
- os.path.dirname(os.path.abspath(script_path)),
363
- ]
364
-
365
- handler = ReloadHandler(script_path, reload_mode)
366
- observer = Observer()
367
-
368
- for d in watch_dirs:
369
- if os.path.exists(d):
370
- if reload_mode:
371
- # 完整重载模式:监控所有.py文件
372
- observer.schedule(handler, d, recursive=True)
373
- else:
374
- # 普通模式:只监控config.toml
375
- observer.schedule(handler, d, recursive=False)
440
+ self._handle_reload(event, "文件变动")
441
+ elif event.src_path.endswith(("config.toml", ".env")):
442
+ self._handle_reload(event, "配置变动")
376
443
 
377
- observer.start()
378
- console.print("\n[bold green][热重载] 已启动[/]")
379
- mode_desc = "开发重载模式" if reload_mode else "配置监控模式"
380
- console.print(f"[dim]模式: {mode_desc}\n监控目录: {', '.join(watch_dirs)}[/]\n")
381
- try:
382
- first_interrupt = True
383
- while True:
384
- time.sleep(1)
385
- except KeyboardInterrupt:
386
- if first_interrupt:
387
- first_interrupt = False
388
- console.print("\n[bold green]正在安全关闭热重载...[/]")
389
- console.print("[bold red]再次按下Ctrl+C以强制关闭[/]")
390
- try:
391
- observer.stop()
392
- if handler.process:
393
- handler.process.terminate()
394
- observer.join()
395
- except KeyboardInterrupt:
396
- console.print("[bold red]强制关闭热重载[/]")
397
- raise
398
- else:
399
- console.print(Panel("[bold red]强制关闭热重载[/]"))
400
- raise
444
+ def _handle_reload(self, event, reason: str):
445
+ """
446
+ 处理重载逻辑
447
+
448
+ :param event: 文件系统事件
449
+ :param reason: 重载原因描述
450
+ """
451
+ console.print(f"\n[reload]{reason}: [path]{event.src_path}[/][/]")
452
+ self._terminate_process()
453
+ self.start_process()
401
454
 
402
- def get_erispulse_version():
455
+ class CLI:
403
456
  """
404
- 获取当前安装的ErisPulse版本
457
+ ErisPulse命令行接口
405
458
 
406
- :return: str ErisPulse版本号或"unknown version"
407
- """
408
- try:
409
- return version("ErisPulse")
410
- except PackageNotFoundError:
411
- return "unknown version"
412
-
413
- def main():
414
- """
415
- CLI主入口
459
+ 提供完整的命令行交互功能
416
460
 
417
- 解析命令行参数并执行相应命令
461
+ {!--< tips >!--}
462
+ 1. 支持动态加载第三方命令
463
+ 2. 支持模块化子命令系统
464
+ {!--< /tips >!--}
418
465
  """
419
466
 
420
- parser = argparse.ArgumentParser(
421
- prog="epsdk",
422
- formatter_class=argparse.RawTextHelpFormatter,
423
- description=f"ErisPulse SDK 命令行工具 {get_erispulse_version()}"
424
- )
425
- parser._positionals.title = "基本"
426
- parser._optionals.title = "可选"
427
-
428
- parser.add_argument("--version", action="store_true", help="显示版本信息")
467
+ def __init__(self):
468
+ """初始化CLI"""
469
+ self.parser = self._create_parser()
470
+ self.package_manager = PackageManager()
471
+ self.observer = None
472
+ self.handler = None
473
+
474
+ def _create_parser(self) -> argparse.ArgumentParser:
475
+ """
476
+ 创建命令行参数解析器
477
+
478
+ :return: 配置好的ArgumentParser实例
479
+ """
480
+ parser = argparse.ArgumentParser(
481
+ prog="epsdk",
482
+ formatter_class=argparse.RawDescriptionHelpFormatter,
483
+ description="ErisPulse SDK 命令行工具\n\n一个功能强大的模块化系统管理工具,用于管理ErisPulse生态系统中的模块、适配器和扩展。",
484
+ )
485
+ parser._positionals.title = "命令"
486
+ parser._optionals.title = "选项"
487
+
488
+ # 全局选项
489
+ parser.add_argument(
490
+ "--version", "-V",
491
+ action="store_true",
492
+ help="显示版本信息"
493
+ )
494
+ parser.add_argument(
495
+ "--verbose", "-v",
496
+ action="count",
497
+ default=0,
498
+ help="增加输出详细程度 (-v, -vv, -vvv)"
499
+ )
500
+
501
+ # 子命令
502
+ subparsers = parser.add_subparsers(
503
+ dest='command',
504
+ metavar="<命令>",
505
+ help="要执行的操作"
506
+ )
507
+
508
+ # 安装命令
509
+ install_parser = subparsers.add_parser(
510
+ 'install',
511
+ help='安装模块/适配器包'
512
+ )
513
+ install_parser.add_argument(
514
+ 'package',
515
+ help='要安装的包名或模块/适配器简称'
516
+ )
517
+ install_parser.add_argument(
518
+ '--upgrade', '-U',
519
+ action='store_true',
520
+ help='升级已安装的包'
521
+ )
522
+ install_parser.add_argument(
523
+ '--pre',
524
+ action='store_true',
525
+ help='包含预发布版本'
526
+ )
527
+
528
+ # 卸载命令
529
+ uninstall_parser = subparsers.add_parser(
530
+ 'uninstall',
531
+ help='卸载模块/适配器包'
532
+ )
533
+ uninstall_parser.add_argument(
534
+ 'package',
535
+ help='要卸载的包名'
536
+ )
537
+
538
+ # 模块管理命令
539
+ module_parser = subparsers.add_parser(
540
+ 'module',
541
+ help='模块管理'
542
+ )
543
+ module_subparsers = module_parser.add_subparsers(
544
+ dest='module_command',
545
+ metavar="<子命令>"
546
+ )
547
+
548
+ # 启用模块
549
+ enable_parser = module_subparsers.add_parser(
550
+ 'enable',
551
+ help='启用模块'
552
+ )
553
+ enable_parser.add_argument(
554
+ 'module',
555
+ help='要启用的模块名'
556
+ )
557
+
558
+ # 禁用模块
559
+ disable_parser = module_subparsers.add_parser(
560
+ 'disable',
561
+ help='禁用模块'
562
+ )
563
+ disable_parser.add_argument(
564
+ 'module',
565
+ help='要禁用的模块名'
566
+ )
567
+
568
+ # 列表命令
569
+ list_parser = subparsers.add_parser(
570
+ 'list',
571
+ help='列出已安装的组件'
572
+ )
573
+ list_parser.add_argument(
574
+ '--type', '-t',
575
+ choices=['modules', 'adapters', 'cli', 'all'],
576
+ default='all',
577
+ help='列出类型 (默认: all)'
578
+ )
579
+ list_parser.add_argument(
580
+ '--outdated', '-o',
581
+ action='store_true',
582
+ help='仅显示可升级的包'
583
+ )
584
+
585
+ # 远程列表命令
586
+ list_remote_parser = subparsers.add_parser(
587
+ 'list-remote',
588
+ help='列出远程可用的组件'
589
+ )
590
+ list_remote_parser.add_argument(
591
+ '--type', '-t',
592
+ choices=['modules', 'adapters', 'cli', 'all'],
593
+ default='all',
594
+ help='列出类型 (默认: all)'
595
+ )
596
+ list_remote_parser.add_argument(
597
+ '--refresh', '-r',
598
+ action='store_true',
599
+ help='强制刷新远程包列表'
600
+ )
601
+
602
+ # 升级命令
603
+ upgrade_parser = subparsers.add_parser(
604
+ 'upgrade',
605
+ help='升级组件'
606
+ )
607
+ upgrade_parser.add_argument(
608
+ 'package',
609
+ nargs='?',
610
+ help='要升级的包名 (可选,不指定则升级所有)'
611
+ )
612
+ upgrade_parser.add_argument(
613
+ '--force', '-f',
614
+ action='store_true',
615
+ help='跳过确认直接升级'
616
+ )
617
+
618
+ # 运行命令
619
+ run_parser = subparsers.add_parser(
620
+ 'run',
621
+ help='运行主程序'
622
+ )
623
+ run_parser.add_argument(
624
+ 'script',
625
+ nargs='?',
626
+ help='要运行的主程序路径 (默认: main.py)'
627
+ )
628
+ run_parser.add_argument(
629
+ '--reload',
630
+ action='store_true',
631
+ help='启用热重载模式'
632
+ )
633
+ run_parser.add_argument(
634
+ '--no-reload',
635
+ action='store_true',
636
+ help='禁用热重载模式'
637
+ )
638
+
639
+ # 初始化命令
640
+ init_parser = subparsers.add_parser(
641
+ 'init',
642
+ help='初始化ErisPulse项目'
643
+ )
644
+ init_parser.add_argument(
645
+ '--force', '-f',
646
+ action='store_true',
647
+ help='强制覆盖现有配置'
648
+ )
649
+
650
+ # 加载第三方命令
651
+ self._load_external_commands(subparsers)
652
+
653
+ return parser
429
654
 
430
- subparsers = parser.add_subparsers(
431
- dest='command',
432
- metavar="<命令>",
433
- )
434
-
435
- # 在main()函数中修改第三方命令加载部分
436
- try:
437
- entry_points = importlib.metadata.entry_points()
438
- if hasattr(entry_points, 'select'):
439
- cli_entries = entry_points.select(group='erispulse.cli')
440
- else:
441
- cli_entries = entry_points.get('erispulse.cli', [])
655
+ def _get_external_commands(self) -> List[str]:
656
+ """
657
+ 获取所有已注册的第三方命令名称
442
658
 
443
- for entry in cli_entries:
444
- try:
445
- cli_func = entry.load()
446
- if callable(cli_func):
447
- # 直接调用函数并传入subparsers和console
448
- cli_func(subparsers, console)
449
- else:
450
- console.print(f"[yellow]模块 {entry.name} 的入口点不是可调用对象[/]")
451
- except Exception as e:
452
- console.print(f"[red]加载第三方命令 {entry.name} 失败: {e}[/]")
453
- import traceback
454
- console.print(traceback.format_exc())
455
- except Exception as e:
456
- console.print(f"[yellow]加载第三方CLI命令失败: {e}[/]", style="warning")
659
+ :return: 第三方命令名称列表
660
+ """
661
+ try:
662
+ entry_points = importlib.metadata.entry_points()
663
+ if hasattr(entry_points, 'select'):
664
+ cli_entries = entry_points.select(group='erispulse.cli')
665
+ else:
666
+ cli_entries = entry_points.get('erispulse.cli', [])
667
+ return [entry.name for entry in cli_entries]
668
+ except Exception:
669
+ return []
457
670
 
458
- # 安装命令
459
- install_parser = subparsers.add_parser('install', help='安装模块/适配器包')
460
- install_parser.add_argument('package', type=str, help='要安装的包名')
461
- install_parser.add_argument('--upgrade', '-U', action='store_true', help='升级已安装的包')
671
+ def _load_external_commands(self, subparsers):
672
+ """
673
+ 加载第三方CLI命令
674
+
675
+ :param subparsers: 子命令解析器
676
+
677
+ :raises ImportError: 加载命令失败时抛出
678
+ """
679
+ try:
680
+ entry_points = importlib.metadata.entry_points()
681
+ if hasattr(entry_points, 'select'):
682
+ cli_entries = entry_points.select(group='erispulse.cli')
683
+ else:
684
+ cli_entries = entry_points.get('erispulse.cli', [])
685
+
686
+ for entry in cli_entries:
687
+ try:
688
+ cli_func = entry.load()
689
+ if callable(cli_func):
690
+ cli_func(subparsers, console)
691
+ else:
692
+ console.print(f"[warning]模块 {entry.name} 的入口点不是可调用对象[/]")
693
+ except Exception as e:
694
+ console.print(f"[error]加载第三方命令 {entry.name} 失败: {e}[/]")
695
+ except Exception as e:
696
+ console.print(f"[warning]加载第三方CLI命令失败: {e}[/]")
462
697
 
463
- # 卸载命令
464
- uninstall_parser = subparsers.add_parser('uninstall', help='卸载模块/适配器包')
465
- uninstall_parser.add_argument('package', type=str, help='要卸载的包名')
698
+ def _print_version(self):
699
+ """打印版本信息"""
700
+ from ErisPulse import __version__
701
+ console.print(Panel(
702
+ f"[title]ErisPulse SDK[/] 版本: [bold]{__version__}[/]",
703
+ subtitle=f"Python {sys.version.split()[0]}",
704
+ style="title"
705
+ ))
466
706
 
467
- # 启用命令
468
- enable_parser = subparsers.add_parser('enable', help='启用模块')
469
- enable_parser.add_argument('module', type=str, help='要启用的模块名')
470
-
471
- # 禁用命令
472
- disable_parser = subparsers.add_parser('disable', help='禁用模块')
473
- disable_parser.add_argument('module', type=str, help='要禁用的模块名')
474
-
475
- # 列表命令
476
- list_parser = subparsers.add_parser('list', help='列出已安装的模块/适配器')
477
- list_parser.add_argument('--type', '-t', choices=['modules', 'adapters', 'all'], default='all',
478
- help='列出类型 (modules: 仅模块, adapters: 仅适配器, all: 全部)')
707
+ def _print_installed_packages(self, pkg_type: str, outdated_only: bool = False):
708
+ """
709
+ 打印已安装包信息
710
+
711
+ :param pkg_type: 包类型 (modules/adapters/cli/all)
712
+ :param outdated_only: 是否只显示可升级的包
713
+ """
714
+ installed = self.package_manager.get_installed_packages()
715
+
716
+ if pkg_type == "modules" and installed["modules"]:
717
+ table = Table(
718
+ title="已安装模块",
719
+ box=SIMPLE,
720
+ header_style="module"
721
+ )
722
+ table.add_column("模块名", style="module")
723
+ table.add_column("包名")
724
+ table.add_column("版本")
725
+ table.add_column("状态")
726
+ table.add_column("描述")
727
+
728
+ for name, info in installed["modules"].items():
729
+ if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
730
+ continue
731
+
732
+ status = "[green]已启用[/]" if info.get("enabled", True) else "[yellow]已禁用[/]"
733
+ table.add_row(
734
+ name,
735
+ info["package"],
736
+ info["version"],
737
+ status,
738
+ info["summary"]
739
+ )
740
+
741
+ console.print(table)
742
+
743
+ if pkg_type == "adapters" and installed["adapters"]:
744
+ table = Table(
745
+ title="已安装适配器",
746
+ box=SIMPLE,
747
+ header_style="adapter"
748
+ )
749
+ table.add_column("适配器名", style="adapter")
750
+ table.add_column("包名")
751
+ table.add_column("版本")
752
+ table.add_column("描述")
753
+
754
+ for name, info in installed["adapters"].items():
755
+ if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
756
+ continue
757
+
758
+ table.add_row(
759
+ name,
760
+ info["package"],
761
+ info["version"],
762
+ info["summary"]
763
+ )
764
+
765
+ console.print(table)
766
+
767
+ if pkg_type == "cli" and installed["cli_extensions"]:
768
+ table = Table(
769
+ title="已安装CLI扩展",
770
+ box=SIMPLE,
771
+ header_style="cli"
772
+ )
773
+ table.add_column("命令名", style="cli")
774
+ table.add_column("包名")
775
+ table.add_column("版本")
776
+ table.add_column("描述")
777
+
778
+ for name, info in installed["cli_extensions"].items():
779
+ if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
780
+ continue
781
+
782
+ table.add_row(
783
+ name,
784
+ info["package"],
785
+ info["version"],
786
+ info["summary"]
787
+ )
788
+
789
+ console.print(table)
479
790
 
480
- # 远程列表命令
481
- list_remote_parser = subparsers.add_parser('list-remote', help='列出远程可用的模块和适配器')
482
- list_remote_parser.add_argument('--type', '-t', choices=['modules', 'adapters', 'all'], default='all',
483
- help='列出类型 (modules: 仅模块, adapters: 仅适配器, all: 全部)')
484
- # 升级命令
485
- upgrade_parser = subparsers.add_parser('upgrade', help='升级所有模块/适配器')
486
- upgrade_parser.add_argument('--force', '-f', action='store_true', help='跳过确认直接升级')
791
+ def _print_remote_packages(self, pkg_type: str):
792
+ """
793
+ 打印远程包信息
794
+
795
+ :param pkg_type: 包类型 (modules/adapters/cli/all)
796
+ """
797
+ remote_packages = asyncio.run(self.package_manager.get_remote_packages())
798
+
799
+ if pkg_type == "modules" and remote_packages["modules"]:
800
+ table = Table(
801
+ title="远程模块",
802
+ box=SIMPLE,
803
+ header_style="module"
804
+ )
805
+ table.add_column("模块名", style="module")
806
+ table.add_column("包名")
807
+ table.add_column("最新版本")
808
+ table.add_column("描述")
809
+
810
+ for name, info in remote_packages["modules"].items():
811
+ table.add_row(
812
+ name,
813
+ info["package"],
814
+ info["version"],
815
+ info["description"]
816
+ )
817
+
818
+ console.print(table)
819
+
820
+ if pkg_type == "adapters" and remote_packages["adapters"]:
821
+ table = Table(
822
+ title="远程适配器",
823
+ box=SIMPLE,
824
+ header_style="adapter"
825
+ )
826
+ table.add_column("适配器名", style="adapter")
827
+ table.add_column("包名")
828
+ table.add_column("最新版本")
829
+ table.add_column("描述")
830
+
831
+ for name, info in remote_packages["adapters"].items():
832
+ table.add_row(
833
+ name,
834
+ info["package"],
835
+ info["version"],
836
+ info["description"]
837
+ )
838
+
839
+ console.print(table)
840
+
841
+ if pkg_type == "cli" and remote_packages.get("cli_extensions"):
842
+ table = Table(
843
+ title="远程CLI扩展",
844
+ box=SIMPLE,
845
+ header_style="cli"
846
+ )
847
+ table.add_column("命令名", style="cli")
848
+ table.add_column("包名")
849
+ table.add_column("最新版本")
850
+ table.add_column("描述")
851
+
852
+ for name, info in remote_packages["cli_extensions"].items():
853
+ table.add_row(
854
+ name,
855
+ info["package"],
856
+ info["version"],
857
+ info["description"]
858
+ )
859
+
860
+ console.print(table)
487
861
 
488
- # 运行命令
489
- run_parser = subparsers.add_parser('run', help='运行指定主程序(默认为main.py)')
490
- run_parser.add_argument('script', type=str, nargs='?', help='要运行的主程序路径(可选,默认为main.py)')
491
- run_parser.add_argument('--reload', action='store_true', help='启用热重载模式')
862
+ def _is_package_outdated(self, package_name: str, current_version: str) -> bool:
863
+ """
864
+ 检查包是否过时
865
+
866
+ :param package_name: 包名
867
+ :param current_version: 当前版本
868
+ :return: 是否有新版本可用
869
+ """
870
+ remote_packages = asyncio.run(self.package_manager.get_remote_packages())
871
+
872
+ # 检查模块
873
+ for module_info in remote_packages["modules"].values():
874
+ if module_info["package"] == package_name:
875
+ return module_info["version"] != current_version
876
+
877
+ # 检查适配器
878
+ for adapter_info in remote_packages["adapters"].values():
879
+ if adapter_info["package"] == package_name:
880
+ return adapter_info["version"] != current_version
881
+
882
+ # 检查CLI扩展
883
+ for cli_info in remote_packages.get("cli_extensions", {}).values():
884
+ if cli_info["package"] == package_name:
885
+ return cli_info["version"] != current_version
886
+
887
+ return False
492
888
 
493
- args = parser.parse_args()
889
+ def _resolve_package_name(self, short_name: str) -> Optional[str]:
890
+ """
891
+ 解析简称到完整包名
892
+
893
+ :param short_name: 模块/适配器简称
894
+ :return: 完整包名,未找到返回None
895
+ """
896
+ remote_packages = asyncio.run(self.package_manager.get_remote_packages())
897
+
898
+ # 检查模块
899
+ if short_name in remote_packages["modules"]:
900
+ return remote_packages["modules"][short_name]["package"]
901
+
902
+ # 检查适配器
903
+ if short_name in remote_packages["adapters"]:
904
+ return remote_packages["adapters"][short_name]["package"]
905
+
906
+ return None
494
907
 
495
- if args.version:
496
- console.print(f"[green]ErisPulse {get_erispulse_version()}[/]")
497
- return
908
+ def _setup_watchdog(self, script_path: str, reload_mode: bool):
909
+ """
910
+ 设置文件监控
911
+
912
+ :param script_path: 要监控的脚本路径
913
+ :param reload_mode: 是否启用重载模式
914
+ """
915
+ watch_dirs = [
916
+ os.path.dirname(os.path.abspath(script_path)),
917
+ ]
918
+
919
+ # 添加配置目录
920
+ config_dir = os.path.abspath(os.getcwd())
921
+ if config_dir not in watch_dirs:
922
+ watch_dirs.append(config_dir)
923
+
924
+ self.handler = ReloadHandler(script_path, reload_mode)
925
+ self.observer = Observer()
926
+
927
+ for d in watch_dirs:
928
+ if os.path.exists(d):
929
+ self.observer.schedule(
930
+ self.handler,
931
+ d,
932
+ recursive=reload_mode
933
+ )
934
+ console.print(f"[dim]监控目录: [path]{d}[/][/]")
935
+
936
+ self.observer.start()
498
937
 
499
- if not args.command:
500
- parser.print_help()
938
+ mode_desc = "[bold]开发重载模式[/]" if reload_mode else "[bold]配置监控模式[/]"
939
+ console.print(Panel(
940
+ f"{mode_desc}\n监控目录: [path]{', '.join(watch_dirs)}[/]",
941
+ title="热重载已启动",
942
+ border_style="info"
943
+ ))
501
944
 
502
- try:
503
- if hasattr(args, 'func'):
504
- args.func(args)
505
- if args.command == "install":
506
- import asyncio
507
- # 首先检查是否是远程模块/适配器的简称
508
- remote_packages = asyncio.run(PyPIManager.get_remote_packages())
509
- full_package_name = None
510
-
511
- # 检查模块
512
- if args.package in remote_packages["modules"]:
513
- full_package_name = remote_packages["modules"][args.package]["package"]
514
- # 检查适配器
515
- elif args.package in remote_packages["adapters"]:
516
- full_package_name = remote_packages["adapters"][args.package]["package"]
517
-
518
- # 如果找到远程包,使用完整包名安装
519
- if full_package_name:
520
- console.print(Panel(
521
- f"找到远程包: [bold]{args.package}[/] → [blue]{full_package_name}[/]",
522
- title="信息",
523
- style="info"
524
- ))
525
- PyPIManager.install_package(full_package_name, args.upgrade)
526
- else:
527
- # 否则按原样安装
528
- PyPIManager.install_package(args.package, args.upgrade)
945
+ def _cleanup(self):
946
+ """清理资源"""
947
+ if self.observer:
948
+ self.observer.stop()
949
+ if self.handler and self.handler.process:
950
+ self.handler._terminate_process()
951
+ self.observer.join()
952
+
953
+ def run(self):
954
+ """
955
+ 运行CLI
956
+
957
+ :raises KeyboardInterrupt: 用户中断时抛出
958
+ :raises Exception: 命令执行失败时抛出
959
+ """
960
+ args = self.parser.parse_args()
961
+
962
+ if args.version:
963
+ self._print_version()
964
+ return
529
965
 
530
- elif args.command == "uninstall":
531
- PyPIManager.uninstall_package(args.package)
532
-
533
- elif args.command == "enable":
534
- from ErisPulse.Core import mods
535
- installed = PyPIManager.get_installed_packages()
536
- if args.module not in installed["modules"]:
537
- console.print(Panel(
538
- f"模块 [red]{args.module}[/] 不存在或未安装",
539
- title="错误",
540
- style="error"
541
- ))
542
- else:
543
- mods.set_module_status(args.module, True)
544
- console.print(Panel(
545
- f"模块 [green]{args.module}[/] 已启用",
546
- title="成功",
547
- style="success"
548
- ))
549
-
550
- elif args.command == "disable":
551
- from ErisPulse.Core import mods
552
- installed = PyPIManager.get_installed_packages()
553
- if args.module not in installed["modules"]:
554
- console.print(Panel(
555
- f"模块 [red]{args.module}[/] 不存在或未安装",
556
- title="错误",
557
- style="error"
558
- ))
559
- else:
560
- mods.set_module_status(args.module, False)
561
- console.print(Panel(
562
- f"模块 [yellow]{args.module}[/] 已禁用",
563
- title="成功",
564
- style="warning"
565
- ))
566
-
567
- elif args.command == "list":
568
- installed = PyPIManager.get_installed_packages()
569
-
570
- if args.type in ["modules", "all"] and installed["modules"]:
571
- table = Table(title="已安装模块", box=SIMPLE)
572
- table.add_column("模块名", style="green")
573
- table.add_column("包名", style="blue")
574
- table.add_column("版本")
575
- table.add_column("描述")
576
-
577
- for name, info in installed["modules"].items():
578
- table.add_row(name, info["package"], info["version"], info["summary"])
579
-
580
- console.print(table)
581
-
582
- if args.type in ["adapters", "all"] and installed["adapters"]:
583
- table = Table(title="已安装适配器", box=SIMPLE)
584
- table.add_column("适配器名", style="yellow")
585
- table.add_column("包名", style="blue")
586
- table.add_column("版本")
587
- table.add_column("描述")
588
-
589
- for name, info in installed["adapters"].items():
590
- table.add_row(name, info["package"], info["version"], info["summary"])
591
-
592
- console.print(table)
593
-
594
- if not installed["modules"] and not installed["adapters"]:
595
- console.print(Panel(
596
- "没有安装任何ErisPulse模块或适配器",
597
- title="提示",
598
- style="info"
599
- ))
600
-
601
- elif args.command == "upgrade":
602
- if args.force or Confirm.ask("确定要升级所有ErisPulse模块和适配器吗?", default=False):
603
- PyPIManager.upgrade_all()
966
+ if not args.command:
967
+ self.parser.print_help()
968
+ return
969
+
970
+ try:
971
+ if args.command == "install":
972
+ full_package = self._resolve_package_name(args.package)
973
+ if full_package:
974
+ console.print(f"[info]找到远程包: [bold]{args.package}[/] → [package]{full_package}[/][/]")
975
+ self.package_manager.install_package(full_package, args.upgrade)
976
+ else:
977
+ self.package_manager.install_package(args.package, args.upgrade)
978
+
979
+ elif args.command == "uninstall":
980
+ self.package_manager.uninstall_package(args.package)
604
981
 
605
- elif args.command == "run":
606
- if not hasattr(args, 'script') or not args.script:
607
- if not os.path.exists("config.toml") or not os.path.isfile("main.py"):
608
- from ErisPulse import sdk
609
- sdk.init()
610
- args.script = "main.py"
611
- console.print(Panel(
612
- "未指定主程序,运行入口点为 [bold]main.py[/]",
613
- title="提示",
614
- style="info"
615
- ))
616
- start_reloader(args.script, args.reload)
617
- elif args.command == "list-remote":
618
- import asyncio
619
- try:
620
- remote_packages = asyncio.run(PyPIManager.get_remote_packages())
982
+ elif args.command == "module":
983
+ from ErisPulse.Core import mods
984
+ installed = self.package_manager.get_installed_packages()
621
985
 
622
- if args.type in ["modules", "all"] and remote_packages["modules"]:
623
- table = Table(title="远程模块", box=SIMPLE)
624
- table.add_column("模块名", style="green")
625
- table.add_column("包名", style="blue")
626
- table.add_column("版本")
627
- table.add_column("描述")
628
-
629
- for name, info in remote_packages["modules"].items():
630
- table.add_row(name, info["package"], info["version"], info["description"])
631
-
632
- console.print(table)
986
+ if args.module_command == "enable":
987
+ if args.module not in installed["modules"]:
988
+ console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
989
+ else:
990
+ mods.set_module_status(args.module, True)
991
+ console.print(f"[success]模块 [bold]{args.module}[/] 已启用[/]")
992
+
993
+ elif args.module_command == "disable":
994
+ if args.module not in installed["modules"]:
995
+ console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
996
+ else:
997
+ mods.set_module_status(args.module, False)
998
+ console.print(f"[warning]模块 [bold]{args.module}[/] 已禁用[/]")
999
+ else:
1000
+ self.parser.parse_args(["module", "--help"])
633
1001
 
634
- if args.type in ["adapters", "all"] and remote_packages["adapters"]:
635
- table = Table(title="远程适配器", box=SIMPLE)
636
- table.add_column("适配器名", style="yellow")
637
- table.add_column("包名", style="blue")
638
- table.add_column("版本")
639
- table.add_column("描述")
1002
+ elif args.command == "list":
1003
+ pkg_type = args.type
1004
+ if pkg_type == "all":
1005
+ self._print_installed_packages("modules", args.outdated)
1006
+ self._print_installed_packages("adapters", args.outdated)
1007
+ self._print_installed_packages("cli", args.outdated)
1008
+ else:
1009
+ self._print_installed_packages(pkg_type, args.outdated)
640
1010
 
641
- for name, info in remote_packages["adapters"].items():
642
- table.add_row(name, info["package"], info["version"], info["description"])
1011
+ elif args.command == "list-remote":
1012
+ pkg_type = args.type
1013
+ if pkg_type == "all":
1014
+ self._print_remote_packages("modules")
1015
+ self._print_remote_packages("adapters")
1016
+ self._print_remote_packages("cli")
1017
+ else:
1018
+ self._print_remote_packages(pkg_type)
643
1019
 
644
- console.print(table)
1020
+ elif args.command == "upgrade":
1021
+ if args.package:
1022
+ self.package_manager.install_package(args.package, upgrade=True)
1023
+ else:
1024
+ if args.force or Confirm.ask("确定要升级所有ErisPulse组件吗?"):
1025
+ self.package_manager.upgrade_all()
1026
+
1027
+ elif args.command == "run":
1028
+ script = args.script or "main.py"
1029
+ if not os.path.exists(script):
1030
+ console.print(f"[error]找不到指定文件: [path]{script}[/][/]")
1031
+ return
645
1032
 
646
- if not remote_packages["modules"] and not remote_packages["adapters"]:
647
- console.print(Panel(
648
- "没有找到远程模块或适配器",
649
- title="提示",
650
- style="info"
651
- ))
1033
+ reload_mode = args.reload and not args.no_reload
1034
+ self._setup_watchdog(script, reload_mode)
1035
+
1036
+ try:
1037
+ while True:
1038
+ time.sleep(1)
1039
+ except KeyboardInterrupt:
1040
+ console.print("\n[info]正在安全关闭...[/]")
1041
+ self._cleanup()
1042
+ console.print("[success]已安全退出[/]")
652
1043
 
653
- except Exception as e:
654
- console.print(Panel(
655
- f"获取远程列表失败: {e}",
656
- title="错误",
657
- style="error"
658
- ))
659
- elif args.command == "init":
660
- from ErisPulse import sdk
661
- sdk.init()
662
- console.print(Panel(
663
- "初始化完成",
664
- title="成功",
665
- style="success"
666
- ))
667
- except KeyboardInterrupt as e:
668
- pass
669
- except Exception as e:
670
- console.print(Panel(
671
- f"执行命令时出错: {e}",
672
- title="错误",
673
- style="error"
674
- ))
1044
+ elif args.command == "init":
1045
+ from ErisPulse import sdk
1046
+ sdk.init()
1047
+ console.print("[success]ErisPulse项目初始化完成[/]")
1048
+
1049
+ # 处理第三方命令
1050
+ elif args.command in self._get_external_commands():
1051
+ # 获取第三方命令的处理函数并执行
1052
+ entry_points = importlib.metadata.entry_points()
1053
+ if hasattr(entry_points, 'select'):
1054
+ cli_entries = entry_points.select(group='erispulse.cli')
1055
+ else:
1056
+ cli_entries = entry_points.get('erispulse.cli', [])
1057
+
1058
+ for entry in cli_entries:
1059
+ if entry.name == args.command:
1060
+ cli_func = entry.load()
1061
+ if callable(cli_func):
1062
+ # 创建一个新的解析器来解析第三方命令的参数
1063
+ subparser = self.parser._subparsers._group_actions[0].choices[args.command]
1064
+ parsed_args = subparser.parse_args(sys.argv[2:])
1065
+ # 调用第三方命令处理函数
1066
+ parsed_args.func(parsed_args)
1067
+ break
1068
+
1069
+ except KeyboardInterrupt:
1070
+ console.print("\n[warning]操作被用户中断[/]")
1071
+ self._cleanup()
1072
+ except Exception as e:
1073
+ console.print(f"[error]执行命令时出错: {e}[/]")
1074
+ if args.verbose >= 1:
1075
+ import traceback
1076
+ console.print(traceback.format_exc())
1077
+ self._cleanup()
1078
+ sys.exit(1)
1079
+
1080
+ def main():
1081
+ """
1082
+ CLI入口点
1083
+
1084
+ {!--< tips >!--}
1085
+ 1. 创建CLI实例并运行
1086
+ 2. 处理全局异常
1087
+ {!--< /tips >!--}
1088
+ """
1089
+ cli = CLI()
1090
+ cli.run()
675
1091
 
676
1092
  if __name__ == "__main__":
677
1093
  main()