bykcli 1.0.0a1__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.
- bykcli-1.0.0a1/LICENSE +21 -0
- bykcli-1.0.0a1/PKG-INFO +47 -0
- bykcli-1.0.0a1/README.md +19 -0
- bykcli-1.0.0a1/pyproject.toml +94 -0
- bykcli-1.0.0a1/setup.cfg +4 -0
- bykcli-1.0.0a1/src/bykcli/__init__.py +3 -0
- bykcli-1.0.0a1/src/bykcli/__main__.py +7 -0
- bykcli-1.0.0a1/src/bykcli/api/__init__.py +38 -0
- bykcli-1.0.0a1/src/bykcli/api/context.py +65 -0
- bykcli-1.0.0a1/src/bykcli/api/network.py +82 -0
- bykcli-1.0.0a1/src/bykcli/api/paths.py +31 -0
- bykcli-1.0.0a1/src/bykcli/app.py +181 -0
- bykcli-1.0.0a1/src/bykcli/core/__init__.py +14 -0
- bykcli-1.0.0a1/src/bykcli/core/context.py +50 -0
- bykcli-1.0.0a1/src/bykcli/core/environment.py +29 -0
- bykcli-1.0.0a1/src/bykcli/core/errors.py +7 -0
- bykcli-1.0.0a1/src/bykcli/core/persistence.py +54 -0
- bykcli-1.0.0a1/src/bykcli/core/state.py +20 -0
- bykcli-1.0.0a1/src/bykcli/infra/__init__.py +0 -0
- bykcli-1.0.0a1/src/bykcli/infra/aliases.py +292 -0
- bykcli-1.0.0a1/src/bykcli/infra/daemon.py +251 -0
- bykcli-1.0.0a1/src/bykcli/infra/logging.py +83 -0
- bykcli-1.0.0a1/src/bykcli/infra/persistence.py +97 -0
- bykcli-1.0.0a1/src/bykcli/infra/registry.py +49 -0
- bykcli-1.0.0a1/src/bykcli/infra/state.py +53 -0
- bykcli-1.0.0a1/src/bykcli/infra/view.py +109 -0
- bykcli-1.0.0a1/src/bykcli/main.py +66 -0
- bykcli-1.0.0a1/src/bykcli/plugins/__init__.py +1 -0
- bykcli-1.0.0a1/src/bykcli/plugins/paths/__init__.py +1 -0
- bykcli-1.0.0a1/src/bykcli/plugins/paths/command.py +43 -0
- bykcli-1.0.0a1/src/bykcli/runtime.py +74 -0
- bykcli-1.0.0a1/src/bykcli/tests/__init__.py +1 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_aliases.py +504 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_cli.py +63 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_daemon.py +550 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_network.py +233 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_paths.py +53 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_persistence.py +184 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_state.py +88 -0
- bykcli-1.0.0a1/src/bykcli/tests/test_view.py +148 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/PKG-INFO +47 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/SOURCES.txt +44 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/dependency_links.txt +1 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/entry_points.txt +3 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/requires.txt +10 -0
- bykcli-1.0.0a1/src/bykcli.egg-info/top_level.txt +1 -0
bykcli-1.0.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bykcli-1.0.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bykcli
|
|
3
|
+
Version: 1.0.0a1
|
|
4
|
+
Summary: A lightweight, extensible collection of CLI utilities
|
|
5
|
+
Author-email: fcbyk <731240932@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.0.0
|
|
19
|
+
Requires-Dist: psutil>=5.9.0
|
|
20
|
+
Requires-Dist: rich>=10.0.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest<8.0.0,>=7.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov<5.0.0,>=4.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: commitizen; extra == "dev"
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
## 简介
|
|
30
|
+
|
|
31
|
+
<p>
|
|
32
|
+
<img src="https://img.shields.io/badge/python-%E2%89%A53.10-blue?logo=python&logoColor=white" />
|
|
33
|
+
<img src="https://img.shields.io/github/license/fcbyk/bykcli.svg" />
|
|
34
|
+
<img src="https://github.com/fcbyk/bykcli/actions/workflows/test.yml/badge.svg" />
|
|
35
|
+
<img src="https://codecov.io/gh/fcbyk/bykcli/branch/main/graph/badge.svg" />
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
**`bykcli`** 是一个轻量的命令行工具集合 🧰
|
|
39
|
+
|
|
40
|
+
用于通过简单命令解决一些日常的小需求。
|
|
41
|
+
|
|
42
|
+
比如:
|
|
43
|
+
* 没有现成工具可用
|
|
44
|
+
* 或者工具太重、不够直接
|
|
45
|
+
* 想一条命令快速搞定
|
|
46
|
+
|
|
47
|
+
项目采用插件化结构,子命令以插件形式组织,支持按需插拔
|
bykcli-1.0.0a1/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## 简介
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<img src="https://img.shields.io/badge/python-%E2%89%A53.10-blue?logo=python&logoColor=white" />
|
|
5
|
+
<img src="https://img.shields.io/github/license/fcbyk/bykcli.svg" />
|
|
6
|
+
<img src="https://github.com/fcbyk/bykcli/actions/workflows/test.yml/badge.svg" />
|
|
7
|
+
<img src="https://codecov.io/gh/fcbyk/bykcli/branch/main/graph/badge.svg" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
**`bykcli`** 是一个轻量的命令行工具集合 🧰
|
|
11
|
+
|
|
12
|
+
用于通过简单命令解决一些日常的小需求。
|
|
13
|
+
|
|
14
|
+
比如:
|
|
15
|
+
* 没有现成工具可用
|
|
16
|
+
* 或者工具太重、不够直接
|
|
17
|
+
* 想一条命令快速搞定
|
|
18
|
+
|
|
19
|
+
项目采用插件化结构,子命令以插件形式组织,支持按需插拔
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bykcli"
|
|
7
|
+
dynamic = ["version", "readme"]
|
|
8
|
+
description = "A lightweight, extensible collection of CLI utilities"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "fcbyk", email = "731240932@qq.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"click>=8.0.0",
|
|
26
|
+
"psutil>=5.9.0",
|
|
27
|
+
"rich>=10.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0.0,<8.0.0",
|
|
33
|
+
"pytest-cov>=4.0.0,<5.0.0",
|
|
34
|
+
"commitizen",
|
|
35
|
+
"build",
|
|
36
|
+
"twine",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
fcbyk = "bykcli.main:main"
|
|
41
|
+
byk = "bykcli.main:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
package-dir = {"" = "src"}
|
|
45
|
+
packages = { find = { where = ["src"], include = ["bykcli*"], exclude = ["example*"] } }
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.dynamic]
|
|
48
|
+
version = {attr = "bykcli.__version__"}
|
|
49
|
+
readme = {file = ["README.md"], content-type = "text/markdown"}
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["src/bykcli/tests"]
|
|
53
|
+
python_files = ["test_*.py"]
|
|
54
|
+
addopts = "-v --cov=bykcli --cov-report=term-missing"
|
|
55
|
+
norecursedirs = ["example", "example-*", ".git", "dist", "build"]
|
|
56
|
+
|
|
57
|
+
[tool.commitizen]
|
|
58
|
+
name = "cz_customize"
|
|
59
|
+
version = "1.0.0a1"
|
|
60
|
+
tag_format = "v$version"
|
|
61
|
+
changelog_file = "CHANGELOG.md"
|
|
62
|
+
bump_message = "release: v$new_version"
|
|
63
|
+
version_files = [
|
|
64
|
+
"src/bykcli/__init__.py:__version__",
|
|
65
|
+
"pyproject.toml:version ="
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.commitizen.customize]
|
|
69
|
+
commit_parser = '^(?P<change_type>feat|fix|refactor|docs|perf|test|style|chore)(?:\((?P<scope>[^\)]+)\))?:\s*(?P<message>.+)$'
|
|
70
|
+
changelog_pattern = '^(feat|fix|refactor|docs|perf|test|style|chore)'
|
|
71
|
+
change_type_order = [
|
|
72
|
+
"feat",
|
|
73
|
+
"fix",
|
|
74
|
+
"refactor",
|
|
75
|
+
"docs",
|
|
76
|
+
"style",
|
|
77
|
+
"perf",
|
|
78
|
+
"test",
|
|
79
|
+
"chore"
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
message_template = """
|
|
83
|
+
{{ change_type }}{% if scope %}({{ scope }}){% endif %}: {{ message }}
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
[tool.commitizen.customize.change_type_map]
|
|
87
|
+
feat = "Features"
|
|
88
|
+
fix = "Bug Fixes"
|
|
89
|
+
refactor = "Refactor"
|
|
90
|
+
docs = "Documentation"
|
|
91
|
+
style = "Styles"
|
|
92
|
+
perf = "Performance"
|
|
93
|
+
test = "Tests"
|
|
94
|
+
chore = "Chores"
|
bykcli-1.0.0a1/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""面向子命令和插件的公开运行时 API。"""
|
|
2
|
+
|
|
3
|
+
from bykcli.api.context import CommandContext, pass_command_context, get_app_context
|
|
4
|
+
from bykcli.api.paths import (
|
|
5
|
+
PathItem,
|
|
6
|
+
PathProvider,
|
|
7
|
+
register_path_provider,
|
|
8
|
+
get_path_provider,
|
|
9
|
+
global_path_items,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from bykcli.api.network import (
|
|
13
|
+
get_private_networks,
|
|
14
|
+
ensure_port_available,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from bykcli.core.state import StateStore
|
|
18
|
+
from bykcli.infra.daemon import start_daemon
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# 上下文
|
|
22
|
+
"CommandContext",
|
|
23
|
+
"pass_command_context",
|
|
24
|
+
"get_app_context",
|
|
25
|
+
# 路径管理
|
|
26
|
+
"PathItem",
|
|
27
|
+
"PathProvider",
|
|
28
|
+
"register_path_provider",
|
|
29
|
+
"get_path_provider",
|
|
30
|
+
"global_path_items",
|
|
31
|
+
# 网络工具
|
|
32
|
+
"get_private_networks",
|
|
33
|
+
"ensure_port_available",
|
|
34
|
+
# 状态存储
|
|
35
|
+
"StateStore",
|
|
36
|
+
# 守护进程
|
|
37
|
+
"start_daemon",
|
|
38
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""面向子命令的上下文封装。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import update_wrapper
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Callable, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from bykcli.app import CliState
|
|
13
|
+
from bykcli.core.context import AppContext, CommandContextLike
|
|
14
|
+
from bykcli.core.state import StateStore
|
|
15
|
+
|
|
16
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class CommandContext(CommandContextLike):
|
|
21
|
+
"""提供给子命令的便捷上下文。"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
app: AppContext
|
|
25
|
+
logger: logging.Logger
|
|
26
|
+
_state_cache: dict[str, StateStore] = field(default_factory=dict, init=False, repr=False)
|
|
27
|
+
|
|
28
|
+
def state(self, name: str = "state") -> StateStore:
|
|
29
|
+
"""获取当前命令的专属存储。"""
|
|
30
|
+
if name not in self._state_cache:
|
|
31
|
+
self._state_cache[name] = self.app.command_store(self.name, name)
|
|
32
|
+
return self._state_cache[name]
|
|
33
|
+
|
|
34
|
+
def _get_group_name(ctx: click.Context) -> str:
|
|
35
|
+
"""获取二级命令名"""
|
|
36
|
+
node = ctx
|
|
37
|
+
while node.parent and node.parent.parent is not None:
|
|
38
|
+
node = node.parent
|
|
39
|
+
return node.command.name or "unknown"
|
|
40
|
+
|
|
41
|
+
def build_command_context(ctx: click.Context) -> CommandContext:
|
|
42
|
+
"""根据当前 Click 上下文构建命令上下文"""
|
|
43
|
+
state = cast(CliState, ctx.obj)
|
|
44
|
+
command_name = _get_group_name(ctx)
|
|
45
|
+
return CommandContext(
|
|
46
|
+
name=command_name,
|
|
47
|
+
app=state.context,
|
|
48
|
+
logger=state.context.get_command_logger(command_name),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pass_command_context(func: F) -> F:
|
|
53
|
+
"""向子命令注入便捷上下文。"""
|
|
54
|
+
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def new_func(ctx: click.Context, *args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
return ctx.invoke(func, build_command_context(ctx), *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
return cast(F, update_wrapper(new_func, func))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_app_context() -> AppContext:
|
|
63
|
+
"""获取当前应用上下文(用于后台进程启动等场景)。"""
|
|
64
|
+
from bykcli.runtime import build_runtime
|
|
65
|
+
return build_runtime()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""网络工具函数。"""
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import psutil
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# 网络接口分类规则:(关键词列表,类型名称,是否虚拟接口,优先级)
|
|
9
|
+
IFACE_RULES = [
|
|
10
|
+
(['vmware', 'vmnet'], 'vmware', True, 30),
|
|
11
|
+
(['vbox', 'virtualbox'], 'virtualbox', True, 30),
|
|
12
|
+
(['docker', 'wsl'], 'container', True, 40),
|
|
13
|
+
(['bluetooth'], 'bluetooth', True, 60),
|
|
14
|
+
(['ethernet', '以太网'], 'ethernet', False, 10),
|
|
15
|
+
(['wlan', 'wi-fi', '无线'], 'wifi', False, 10),
|
|
16
|
+
(['loopback'], 'loopback', True, 100),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_iface_type(iface: str) -> tuple[str, bool, int]:
|
|
21
|
+
"""检测网络接口类型。"""
|
|
22
|
+
lname = iface.lower()
|
|
23
|
+
for keywords, t, virtual, prio in IFACE_RULES:
|
|
24
|
+
if any(k in lname for k in keywords):
|
|
25
|
+
return t, virtual, prio
|
|
26
|
+
return 'unknown', False, 50
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_private_networks() -> List[Dict]:
|
|
30
|
+
"""获取所有局域网 IP 地址信息。"""
|
|
31
|
+
results = []
|
|
32
|
+
interfaces = psutil.net_if_addrs()
|
|
33
|
+
|
|
34
|
+
for iface, addrs in interfaces.items():
|
|
35
|
+
ips = []
|
|
36
|
+
iface_type, is_virtual, priority = detect_iface_type(iface)
|
|
37
|
+
|
|
38
|
+
for addr in addrs:
|
|
39
|
+
if addr.family != socket.AF_INET:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
ip = addr.address
|
|
43
|
+
|
|
44
|
+
# 跳过回环地址和链路本地地址
|
|
45
|
+
if ip.startswith('127.'):
|
|
46
|
+
continue
|
|
47
|
+
if ip.startswith('169.254.'):
|
|
48
|
+
continue
|
|
49
|
+
# 只保留私有 IP 地址
|
|
50
|
+
if ip.startswith(('10.', '192.168.', '172.')):
|
|
51
|
+
ips.append(ip)
|
|
52
|
+
|
|
53
|
+
if ips:
|
|
54
|
+
results.append({
|
|
55
|
+
"iface": iface,
|
|
56
|
+
"ips": ips,
|
|
57
|
+
"type": iface_type,
|
|
58
|
+
"virtual": is_virtual,
|
|
59
|
+
"priority": priority
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
# 按优先级排序(数字越小优先级越高)
|
|
63
|
+
results.sort(key=lambda x: x['priority'])
|
|
64
|
+
|
|
65
|
+
# 如果没有找到任何局域网 IP,返回 localhost
|
|
66
|
+
if not results:
|
|
67
|
+
results.append({
|
|
68
|
+
"iface": "localhost",
|
|
69
|
+
"ips": ["127.0.0.1"],
|
|
70
|
+
"type": "loopback",
|
|
71
|
+
"virtual": True,
|
|
72
|
+
"priority": 100
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def ensure_port_available(port: int, host: str = "0.0.0.0") -> None:
|
|
79
|
+
"""检查端口是否可用。"""
|
|
80
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
81
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
82
|
+
s.bind((host, int(port)))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""路径展示注册与渲染。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from bykcli.core.context import AppContext
|
|
8
|
+
|
|
9
|
+
PathItem = tuple[str, str]
|
|
10
|
+
PathProvider = Callable[[AppContext], list[PathItem]]
|
|
11
|
+
|
|
12
|
+
_PATH_PROVIDERS: dict[str, PathProvider] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_path_provider(command_name: str, provider: PathProvider) -> None:
|
|
16
|
+
"""注册子命令路径提供器。"""
|
|
17
|
+
_PATH_PROVIDERS[command_name] = provider
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_path_provider(command_name: str) -> PathProvider | None:
|
|
21
|
+
"""获取某个子命令的路径提供器。"""
|
|
22
|
+
return _PATH_PROVIDERS.get(command_name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def global_path_items(context: AppContext) -> list[PathItem]:
|
|
26
|
+
"""返回默认展示的全局路径。"""
|
|
27
|
+
return [
|
|
28
|
+
("CLI Home", str(context.paths.root_dir)),
|
|
29
|
+
("Alias File", str(context.paths.alias_file)),
|
|
30
|
+
("Logs Directory", str(context.paths.logs_dir)),
|
|
31
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""CLI 应用装配。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from bykcli.core.context import AppContext
|
|
10
|
+
from bykcli.infra.aliases import AliasAwareGroup
|
|
11
|
+
from bykcli.infra.daemon import kill_daemon_callback
|
|
12
|
+
from bykcli.infra.logging import setup_logging
|
|
13
|
+
from bykcli.infra.registry import register_builtin_plugins, register_plugins
|
|
14
|
+
from bykcli.infra.view import render_dashboard
|
|
15
|
+
from bykcli.runtime import build_runtime
|
|
16
|
+
import bykcli.plugins as builtin_plugins
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class CliState:
|
|
21
|
+
"""Click 根命令共享状态。"""
|
|
22
|
+
|
|
23
|
+
context: AppContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def version_callback(
|
|
27
|
+
ctx: click.Context,
|
|
28
|
+
_param: click.Parameter,
|
|
29
|
+
value: bool,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Show version and exit."""
|
|
32
|
+
if not value or ctx.resilient_parsing:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
from bykcli.infra.view import format_version_line
|
|
36
|
+
from rich.console import Console
|
|
37
|
+
|
|
38
|
+
app_context: AppContext = ctx.obj.context if ctx.obj else build_runtime()
|
|
39
|
+
console = Console()
|
|
40
|
+
console.print(format_version_line(app_context.environment))
|
|
41
|
+
ctx.exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def list_plugins_callback(
|
|
45
|
+
ctx: click.Context,
|
|
46
|
+
_param: click.Parameter,
|
|
47
|
+
value: bool,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""List all external plugins and their versions."""
|
|
50
|
+
if not value or ctx.resilient_parsing:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
from importlib.metadata import entry_points, version, PackageNotFoundError, distributions
|
|
54
|
+
from rich.console import Console
|
|
55
|
+
from rich.table import Table
|
|
56
|
+
from rich import box
|
|
57
|
+
|
|
58
|
+
console = Console()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
plugin_entries = entry_points(group="bykcli.plugins")
|
|
62
|
+
except TypeError:
|
|
63
|
+
plugin_entries = entry_points().get("bykcli.plugins", [])
|
|
64
|
+
|
|
65
|
+
if not plugin_entries:
|
|
66
|
+
console.print("[dim]No external plugins installed.[/dim]")
|
|
67
|
+
ctx.exit()
|
|
68
|
+
|
|
69
|
+
table = Table(
|
|
70
|
+
title="[bold cyan]External Plugins[/bold cyan]",
|
|
71
|
+
box=box.ROUNDED,
|
|
72
|
+
border_style="bright_blue",
|
|
73
|
+
header_style="bold bright_magenta",
|
|
74
|
+
show_lines=True,
|
|
75
|
+
padding=(0, 2),
|
|
76
|
+
collapse_padding=False,
|
|
77
|
+
)
|
|
78
|
+
table.add_column("Package Name", style="bold cyan", no_wrap=True)
|
|
79
|
+
table.add_column("Version", style="bold green", justify="center")
|
|
80
|
+
table.add_column("Entry Point", style="dim", overflow="fold")
|
|
81
|
+
|
|
82
|
+
for entry in plugin_entries:
|
|
83
|
+
pkg_name = entry.name
|
|
84
|
+
pkg_version = "unknown"
|
|
85
|
+
|
|
86
|
+
candidates = [entry.name]
|
|
87
|
+
|
|
88
|
+
if entry.name.startswith("bykcli"):
|
|
89
|
+
candidates.append(f"bykcli-{entry.name[5:]}")
|
|
90
|
+
else:
|
|
91
|
+
candidates.append(f"bykcli-{entry.name}")
|
|
92
|
+
|
|
93
|
+
candidates.append(entry.name.replace("-", "_"))
|
|
94
|
+
candidates.append(entry.name.replace("_", "-"))
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
module_path = entry.value.split(":")[0]
|
|
98
|
+
top_level_pkg = module_path.split(".")[0]
|
|
99
|
+
candidates.append(top_level_pkg)
|
|
100
|
+
if not top_level_pkg.startswith("bykcli"):
|
|
101
|
+
candidates.append(f"bykcli-{top_level_pkg}")
|
|
102
|
+
except (IndexError):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
found = False
|
|
106
|
+
for candidate in candidates:
|
|
107
|
+
if not candidate:
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
pkg_version = version(candidate)
|
|
111
|
+
pkg_name = candidate
|
|
112
|
+
found = True
|
|
113
|
+
break
|
|
114
|
+
except PackageNotFoundError:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
table.add_row(pkg_name, pkg_version, entry.value)
|
|
118
|
+
|
|
119
|
+
click.echo()
|
|
120
|
+
console.print(table)
|
|
121
|
+
click.echo()
|
|
122
|
+
ctx.exit()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_cli() -> click.Group:
|
|
126
|
+
"""创建根 CLI 对象。"""
|
|
127
|
+
|
|
128
|
+
runtime: AppContext | None = None
|
|
129
|
+
|
|
130
|
+
def get_runtime() -> AppContext:
|
|
131
|
+
nonlocal runtime
|
|
132
|
+
if runtime is None:
|
|
133
|
+
runtime = build_runtime()
|
|
134
|
+
return runtime
|
|
135
|
+
|
|
136
|
+
temp_runtime = build_runtime()
|
|
137
|
+
setup_logging(temp_runtime)
|
|
138
|
+
|
|
139
|
+
@click.group(
|
|
140
|
+
cls=AliasAwareGroup,
|
|
141
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
142
|
+
invoke_without_command=True,
|
|
143
|
+
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"--version",
|
|
146
|
+
"-v",
|
|
147
|
+
is_flag=True,
|
|
148
|
+
callback=version_callback,
|
|
149
|
+
expose_value=False,
|
|
150
|
+
is_eager=True,
|
|
151
|
+
help="Show version and exit.",
|
|
152
|
+
)
|
|
153
|
+
@click.option(
|
|
154
|
+
"--kill",
|
|
155
|
+
"-k",
|
|
156
|
+
type=str,
|
|
157
|
+
callback=kill_daemon_callback,
|
|
158
|
+
expose_value=False,
|
|
159
|
+
is_eager=True,
|
|
160
|
+
help='Kill background daemon processes. Use "all" to kill all or specify PID.',
|
|
161
|
+
)
|
|
162
|
+
@click.option(
|
|
163
|
+
"--list",
|
|
164
|
+
"-l",
|
|
165
|
+
is_flag=True,
|
|
166
|
+
callback=list_plugins_callback,
|
|
167
|
+
expose_value=False,
|
|
168
|
+
is_eager=True,
|
|
169
|
+
help="List all external plugins and their versions.",
|
|
170
|
+
)
|
|
171
|
+
@click.pass_context
|
|
172
|
+
def cli(ctx: click.Context) -> None:
|
|
173
|
+
ctx.obj = CliState(context=get_runtime())
|
|
174
|
+
ctx.obj.context.logger.info("BYK CLI v%s started", ctx.obj.context.version)
|
|
175
|
+
if ctx.invoked_subcommand is None:
|
|
176
|
+
render_dashboard(ctx.obj.context, cli)
|
|
177
|
+
|
|
178
|
+
cli.runtime_provider = get_runtime
|
|
179
|
+
register_builtin_plugins(cli, builtin_plugins)
|
|
180
|
+
register_plugins(cli)
|
|
181
|
+
return cli
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""核心能力模块。"""
|
|
2
|
+
|
|
3
|
+
from bykcli.core.context import AppContext, CommandContextLike
|
|
4
|
+
from bykcli.core.environment import EnvironmentInfo
|
|
5
|
+
from bykcli.core.persistence import PathLayout
|
|
6
|
+
from bykcli.core.state import StateStore
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AppContext",
|
|
10
|
+
"CommandContextLike",
|
|
11
|
+
"EnvironmentInfo",
|
|
12
|
+
"PathLayout",
|
|
13
|
+
"StateStore",
|
|
14
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI 运行时上下文。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Protocol
|
|
8
|
+
|
|
9
|
+
from bykcli.core.environment import EnvironmentInfo
|
|
10
|
+
from bykcli.core.persistence import PathLayout
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from bykcli.core.state import StateStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandContextLike(Protocol):
|
|
17
|
+
"""命令上下文协议。"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
app: "AppContext"
|
|
21
|
+
logger: logging.Logger
|
|
22
|
+
|
|
23
|
+
def state(self, name: str = "state") -> "StateStore": ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class AppContext:
|
|
28
|
+
"""子命令共享的核心上下文。"""
|
|
29
|
+
|
|
30
|
+
app_name: str
|
|
31
|
+
version: str
|
|
32
|
+
paths: PathLayout
|
|
33
|
+
environment: EnvironmentInfo
|
|
34
|
+
logger: logging.Logger
|
|
35
|
+
|
|
36
|
+
def command_store(
|
|
37
|
+
self,
|
|
38
|
+
command_name: str,
|
|
39
|
+
name: str = "state",
|
|
40
|
+
) -> StateStore:
|
|
41
|
+
"""返回某个子命令的状态存储。"""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def store(self, name: str = "byk.config") -> StateStore:
|
|
45
|
+
"""返回应用级共享状态存储。"""
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
def get_command_logger(self, command_name: str) -> logging.Logger:
|
|
49
|
+
"""获取命令专属 logger。"""
|
|
50
|
+
raise NotImplementedError
|