feat(cli): 重构CLI并添加富文本输出与版本/帮助优化
CI / Lint, Typecheck & Test (push) Has been cancelled

- 使用rich库重构工具列表展示为美观的表格形式
- 添加--version/-V参数显示版本号
- 新增未知工具的模糊匹配提示功能
- 统一使用标准错误输出和富文本样式打印错误信息
- 优化命令行参数处理逻辑,支持先解析help/version参数
This commit is contained in:
2026-07-06 22:16:12 +08:00
parent fca6f17a5c
commit 4721b59f40
+75 -38
View File
@@ -14,10 +14,17 @@
from __future__ import annotations
import contextlib
import difflib
import importlib
import sys
from collections.abc import Sequence
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from pyflowx import __version__
from pyflowx.tools import _TOOL_REGISTRY, run_tool
@@ -97,6 +104,8 @@ class PfApp:
def __init__(self, argv: Sequence[str] | None = None) -> None:
self._argv = list(argv) if argv is not None else sys.argv[1:]
self._console = Console()
self._err = Console(stderr=True)
def run(self) -> int:
"""主入口, 返回退出码."""
@@ -104,44 +113,67 @@ class PfApp:
self._list_tools()
return 0
tool_name = self._argv[0]
rest_argv = self._argv[1:]
first = self._argv[0]
resolved = self._resolve_tool(tool_name)
if first in ("--help", "-h"):
self._list_tools()
return 0
if first in ("--version", "-V"):
self._console.print(f"PyFlowX [bold cyan]{__version__}[/bold cyan]")
return 0
rest = self._argv[1:]
resolved = self._resolve_tool(first)
if resolved is None:
print(f"错误: 未知工具 '{tool_name}'", file=sys.stderr)
print("运行 'pf' 查看可用工具列表", file=sys.stderr)
self._print_unknown_tool(first)
return 1
tool_type, target = resolved
if tool_type == "legacy":
return self._run_legacy(target, rest_argv)
return self._run_tool(target, rest_argv)
kind, target = resolved
if kind == "legacy":
return self._run_legacy(target, rest)
return self._run_tool(target, rest)
# ------------------------------------------------------------------ #
# 工具列表 (rich)
# ------------------------------------------------------------------ #
def _list_tools(self) -> None:
"""列出所有可用工具."""
print("PyFlowX 工具列表:")
print()
print("@px.tool 工具:")
yaml_tools = sorted(set(self._TOOL_ALIASES.values()))
for tool in yaml_tools:
description = self._tool_description(tool)
if description:
print(f" pf {tool:<15} - {description}")
else:
print(f" pf {tool:<15}")
print()
print("传统工具:")
for tool in sorted(self._LEGACY_TOOLS):
print(f" pf {tool:<15}")
print()
print("示例:")
print(" pf filedate add a.txt")
print(" pf pymake b")
"""rich 表格列出所有可用工具."""
self._console.print(
Panel(
Text(f"PyFlowX v{__version__}", style="bold cyan", justify="center"),
subtitle="[dim]pf <tool> [command] [options][/dim]",
)
)
table = Table(title="@px.tool 工具", show_header=True, header_style="bold", show_lines=False)
table.add_column("命令", style="cyan", no_wrap=True)
table.add_column("别名", style="dim", no_wrap=True)
table.add_column("说明")
for tool in sorted(set(self._TOOL_ALIASES.values())):
aliases = self._aliases_for(tool)
table.add_row(f"pf {tool}", ", ".join(aliases), self._tool_description(tool))
self._console.print(table)
if self._LEGACY_TOOLS:
legacy = Table(title="传统工具", show_header=True, header_style="bold", show_lines=False)
legacy.add_column("命令", style="cyan", no_wrap=True)
for tool in sorted(self._LEGACY_TOOLS):
legacy.add_row(f"pf {tool}")
self._console.print(legacy)
self._console.print("\n[bold]示例:[/bold]")
self._console.print(" [cyan]pf filedate add a.txt[/cyan] # 给文件添加日期前缀")
self._console.print(" [cyan]pf pymake b[/cyan] # 构建 Python 包")
self._console.print(" [cyan]pf gitt c[/cyan] # 清理并查看 git 状态")
def _aliases_for(self, canonical: str) -> list[str]:
"""获取工具的别名 (不含规范名本身)."""
return sorted(a for a, t in self._TOOL_ALIASES.items() if t == canonical and a != canonical)
def _tool_description(self, tool_name: str) -> str:
"""获取工具描述 (从 _TOOL_REGISTRY 中已注册 ToolSpec 的 description/help)."""
# 触发 @px.tool 注册
with contextlib.suppress(ImportError):
importlib.import_module(f"pyflowx.ops.{tool_name}")
@@ -149,28 +181,33 @@ class PfApp:
return ""
subs = _TOOL_REGISTRY[tool_name]
# 优先取 description, 否则取任一 subcommand 的 help
for spec in subs.values():
if spec.description:
return spec.description
# 取第一个非 hidden subcommand 的 help
for spec in subs.values():
if not spec.hidden and spec.help:
return spec.help
return ""
# ------------------------------------------------------------------ #
# 路由
# ------------------------------------------------------------------ #
def _resolve_tool(self, name: str) -> tuple[str, str] | None:
"""解析工具名, 返回 (类型, 目标).
类型: "tool""legacy"
目标: ops 模块名或 legacy 模块路径
"""
"""解析工具名, 返回 (类型, 目标)."""
if name in self._TOOL_ALIASES:
return ("tool", self._TOOL_ALIASES[name])
if name in self._LEGACY_TOOLS:
return ("legacy", self._LEGACY_TOOLS[name])
return None
def _print_unknown_tool(self, name: str) -> None:
"""打印未知工具错误 + 模糊匹配建议."""
self._err.print(f"[red]错误:[/red] 未知工具 [yellow]{name!r}[/yellow]")
suggestions = difflib.get_close_matches(name, list(self._TOOL_ALIASES), n=3, cutoff=0.5)
if suggestions:
self._err.print(f"[dim]是否想用: {', '.join(suggestions)}[/dim]")
self._err.print("[dim]运行 'pf' 查看可用工具列表[/dim]")
def _run_legacy(self, module_path: str, argv: list[str]) -> int:
"""运行传统工具的 main() 函数."""
module_name, func_name = module_path.split(":", 1)
@@ -196,8 +233,8 @@ class PfApp:
importlib.import_module(f"pyflowx.ops.{target}")
if target not in _TOOL_REGISTRY:
print(f"错误: 未找到工具 '{target}'", file=sys.stderr)
print("运行 'pf' 查看可用工具列表", file=sys.stderr)
self._err.print(f"[red]错误:[/red] 未找到工具 [yellow]{target!r}[/yellow]")
self._err.print("[dim]运行 'pf' 查看可用工具列表[/dim]")
return 1
return run_tool(target, argv)