aukeys-opscli 0.0.6__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.
Files changed (92) hide show
  1. aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
  2. aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
  3. aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
  4. aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
  5. opscli/__init__.py +9 -0
  6. opscli/amazon/__init__.py +13 -0
  7. opscli/amazon/cli.py +5 -0
  8. opscli/amazon/client.py +5 -0
  9. opscli/amazon/commands/__init__.py +1 -0
  10. opscli/amazon/commands/cli.py +200 -0
  11. opscli/amazon/domain/__init__.py +25 -0
  12. opscli/amazon/domain/exceptions.py +73 -0
  13. opscli/amazon/domain/models.py +74 -0
  14. opscli/amazon/exceptions.py +21 -0
  15. opscli/amazon/manager.py +5 -0
  16. opscli/amazon/models.py +5 -0
  17. opscli/amazon/parser.py +5 -0
  18. opscli/amazon/scraper.py +5 -0
  19. opscli/amazon/scraping/__init__.py +12 -0
  20. opscli/amazon/scraping/parser.py +66 -0
  21. opscli/amazon/scraping/scraper.py +315 -0
  22. opscli/amazon/services/__init__.py +5 -0
  23. opscli/amazon/services/manager.py +105 -0
  24. opscli/amazon/transport/__init__.py +5 -0
  25. opscli/amazon/transport/client.py +77 -0
  26. opscli/auth/__init__.py +79 -0
  27. opscli/auth/cli.py +255 -0
  28. opscli/auth/commands/__init__.py +1 -0
  29. opscli/auth/commands/cli.py +5 -0
  30. opscli/auth/config.py +55 -0
  31. opscli/auth/core/__init__.py +1 -0
  32. opscli/auth/core/device_flow.py +80 -0
  33. opscli/auth/core/system_registry.py +89 -0
  34. opscli/auth/core/token_manager.py +167 -0
  35. opscli/auth/domain/__init__.py +23 -0
  36. opscli/auth/domain/exceptions.py +33 -0
  37. opscli/auth/exceptions.py +23 -0
  38. opscli/auth/storage/__init__.py +1 -0
  39. opscli/auth/storage/credential_store.py +163 -0
  40. opscli/auth/storage/crypto.py +67 -0
  41. opscli/cli.py +36 -0
  42. opscli/config.py +3 -0
  43. opscli/query/__init__.py +6 -0
  44. opscli/query/cli.py +5 -0
  45. opscli/query/client.py +5 -0
  46. opscli/query/commands/__init__.py +1 -0
  47. opscli/query/commands/cli.py +140 -0
  48. opscli/query/domain/__init__.py +23 -0
  49. opscli/query/domain/exceptions.py +73 -0
  50. opscli/query/domain/models.py +21 -0
  51. opscli/query/exceptions.py +21 -0
  52. opscli/query/manager.py +5 -0
  53. opscli/query/models.py +5 -0
  54. opscli/query/services/__init__.py +5 -0
  55. opscli/query/services/manager.py +373 -0
  56. opscli/query/transport/__init__.py +5 -0
  57. opscli/query/transport/client.py +57 -0
  58. opscli/skills/__init__.py +37 -0
  59. opscli/skills/cli.py +5 -0
  60. opscli/skills/commands/__init__.py +1 -0
  61. opscli/skills/commands/cli.py +389 -0
  62. opscli/skills/detector.py +5 -0
  63. opscli/skills/discovery/__init__.py +5 -0
  64. opscli/skills/discovery/detector.py +225 -0
  65. opscli/skills/domain/__init__.py +23 -0
  66. opscli/skills/domain/exceptions.py +51 -0
  67. opscli/skills/domain/models.py +144 -0
  68. opscli/skills/exceptions.py +5 -0
  69. opscli/skills/manager.py +5 -0
  70. opscli/skills/models.py +19 -0
  71. opscli/skills/services/__init__.py +5 -0
  72. opscli/skills/services/manager.py +276 -0
  73. opscli/skills/sync/__init__.py +5 -0
  74. opscli/skills/sync/updater.py +274 -0
  75. opscli/skills/templates/ops-amazon/SKILL.md +181 -0
  76. opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
  77. opscli/skills/templates/ops-auth/SKILL.md +466 -0
  78. opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
  79. opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
  80. opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
  81. opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
  82. opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
  83. opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
  84. opscli/skills/templates/ops-dataset-query/references//346/225/260/346/215/256/346/237/245/350/257/242/346/234/215/345/212/241/345/274/200/345/217/221/350/257/264/346/230/216/346/226/207/346/241/243.md +1126 -0
  85. opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
  86. opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
  87. opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
  88. opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
  89. opscli/skills/templates/ops-skills/SKILL.md +494 -0
  90. opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
  91. opscli/skills/updater.py +5 -0
  92. opscli/version.py +19 -0
@@ -0,0 +1,51 @@
1
+ """skills 模块异常定义。
2
+
3
+ 统一封装对外错误对象,保证 CLI JSON 输出结构稳定。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ class SkillsError(Exception):
10
+ """skills 模块统一异常基类。"""
11
+
12
+ def to_dict(self) -> dict[str, object]:
13
+ """转换为稳定的错误对象,便于 CLI 直接输出 JSON。"""
14
+ return {
15
+ "type": self.__class__.__name__,
16
+ "message": str(self),
17
+ }
18
+
19
+
20
+ class SkillRemoteError(SkillsError):
21
+ """远端 Skill 接口异常。"""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ *,
27
+ endpoint: str | None = None,
28
+ status_code: int | None = None,
29
+ ) -> None:
30
+ super().__init__(message)
31
+ self.endpoint = endpoint
32
+ self.status_code = status_code
33
+
34
+ def to_dict(self) -> dict[str, object]:
35
+ """输出附带远端定位信息的错误对象。"""
36
+ payload = super().to_dict()
37
+ if self.endpoint is not None:
38
+ payload["endpoint"] = self.endpoint
39
+ if self.status_code is not None:
40
+ payload["status_code"] = self.status_code
41
+ return payload
42
+
43
+
44
+ def error_to_dict(exc: Exception) -> dict[str, object]:
45
+ """将任意异常转换为稳定的错误对象。"""
46
+ if isinstance(exc, SkillsError):
47
+ return exc.to_dict()
48
+ return {
49
+ "type": exc.__class__.__name__,
50
+ "message": str(exc),
51
+ }
@@ -0,0 +1,144 @@
1
+ """Skills 模块的数据模型定义。
2
+
3
+ 包含 Skill 生命周期中各阶段的数据结构:发现记录、安装结果、升级结果。
4
+ 所有 dataclass 均提供 to_dict() 方法用于 JSON 序列化输出。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict, dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ def runtime_to_tool_name(runtime: str) -> str:
14
+ """将内部 runtime 标识转换为对外稳定的工具名。"""
15
+ mapping = {
16
+ "claude": "claude-code",
17
+ "openclaw": "openclaw",
18
+ }
19
+ return mapping.get(runtime, runtime)
20
+
21
+
22
+ @dataclass
23
+ class SkillRecord:
24
+ """已发现的 Skill 记录。
25
+
26
+ 由 SkillDetector 扫描磁盘目录后生成,描述一个已安装 Skill 的基本信息。
27
+ """
28
+
29
+ name: str # Skill 名称,对应目录名
30
+ version: str # 当前版本号,读取自 data/VERSION.json
31
+ runtime: str # 运行时类型:claude 或 openclaw
32
+ root: Path # Skill 根目录绝对路径
33
+ version_file: Path # VERSION.json 文件绝对路径
34
+
35
+ def to_dict(self) -> dict:
36
+ """转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
37
+ data = asdict(self)
38
+ data["runtime"] = runtime_to_tool_name(self.runtime)
39
+ data["root"] = str(self.root)
40
+ data["version_file"] = str(self.version_file)
41
+ return data
42
+
43
+
44
+ @dataclass
45
+ class SkillInstallResult:
46
+ """Skill 安装操作的结果。
47
+
48
+ 由 SkillsManager.install() 返回,记录安装目标路径及是否覆盖了已有安装。
49
+ """
50
+
51
+ name: str # Skill 名称
52
+ runtime: str # 目标运行时类型
53
+ target_dir: Path # 安装目标目录
54
+ version: str # 安装后的版本号
55
+ replaced: bool # 是否覆盖了已有安装
56
+
57
+ def to_dict(self) -> dict:
58
+ """转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
59
+ data = asdict(self)
60
+ data["runtime"] = runtime_to_tool_name(self.runtime)
61
+ data["target_dir"] = str(self.target_dir)
62
+ return data
63
+
64
+
65
+ @dataclass
66
+ class SkillBatchInstallResult:
67
+ """批量安装结果。"""
68
+
69
+ name: str
70
+ installs: list[SkillInstallResult]
71
+
72
+ def to_dict(self) -> dict:
73
+ return {
74
+ "name": self.name,
75
+ "version": self.installs[0].version if self.installs else "unknown",
76
+ "installed_paths": [
77
+ {
78
+ "tool": runtime_to_tool_name(item.runtime),
79
+ "path": str(item.target_dir),
80
+ "replaced": item.replaced,
81
+ }
82
+ for item in self.installs
83
+ ],
84
+ }
85
+
86
+
87
+ @dataclass
88
+ class SkillUpgradeResult:
89
+ """Skill 升级操作的结果。
90
+
91
+ 由 SkillsUpdater.upgrade_dataset_fields() 返回,记录版本变化和是否实际更新。
92
+ """
93
+
94
+ name: str # Skill 名称
95
+ from_version: str # 升级前版本号
96
+ to_version: str # 升级后版本号(未更新时与 from_version 相同)
97
+ runtime: str # 所属运行时
98
+ target_dir: Path # Skill 根目录
99
+ updated: bool # 是否实际执行了更新
100
+ field_count: int = 0 # 当前用户可见字段数
101
+
102
+ def to_dict(self) -> dict:
103
+ """转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
104
+ data = asdict(self)
105
+ data["runtime"] = runtime_to_tool_name(self.runtime)
106
+ data["target_dir"] = str(self.target_dir)
107
+ return data
108
+
109
+
110
+ @dataclass
111
+ class SkillBatchUpgradeResult:
112
+ """批量升级结果。"""
113
+
114
+ name: str
115
+ results: list[SkillUpgradeResult]
116
+
117
+ def to_dict(self) -> dict:
118
+ updated = self._group_results(updated=True)
119
+ already_latest = self._group_results(updated=False)
120
+ return {
121
+ "updated": updated,
122
+ "already_latest": already_latest,
123
+ "failed": [],
124
+ }
125
+
126
+ def _group_results(self, *, updated: bool) -> list[dict]:
127
+ grouped: dict[tuple[str, str, str, int], dict] = {}
128
+ for item in self.results:
129
+ if item.updated is not updated:
130
+ continue
131
+ key = (item.name, item.from_version, item.to_version, item.field_count)
132
+ group = grouped.setdefault(
133
+ key,
134
+ {
135
+ "name": item.name,
136
+ "from_version": item.from_version,
137
+ "to_version": item.to_version,
138
+ "field_count": item.field_count,
139
+ "tools": [],
140
+ },
141
+ )
142
+ group["tools"].append(runtime_to_tool_name(item.runtime))
143
+
144
+ return list(grouped.values())
@@ -0,0 +1,5 @@
1
+ """skills exceptions 兼容导出。"""
2
+
3
+ from opscli.skills.domain.exceptions import SkillRemoteError, SkillsError, error_to_dict
4
+
5
+ __all__ = ["SkillsError", "SkillRemoteError", "error_to_dict"]
@@ -0,0 +1,5 @@
1
+ """skills manager 兼容导出。"""
2
+
3
+ from opscli.skills.services.manager import SkillsManager
4
+
5
+ __all__ = ["SkillsManager"]
@@ -0,0 +1,19 @@
1
+ """skills models 兼容导出。"""
2
+
3
+ from opscli.skills.domain.models import (
4
+ SkillBatchInstallResult,
5
+ SkillBatchUpgradeResult,
6
+ SkillInstallResult,
7
+ SkillRecord,
8
+ SkillUpgradeResult,
9
+ runtime_to_tool_name,
10
+ )
11
+
12
+ __all__ = [
13
+ "runtime_to_tool_name",
14
+ "SkillRecord",
15
+ "SkillInstallResult",
16
+ "SkillBatchInstallResult",
17
+ "SkillUpgradeResult",
18
+ "SkillBatchUpgradeResult",
19
+ ]
@@ -0,0 +1,5 @@
1
+ """skills 服务层。"""
2
+
3
+ from opscli.skills.services.manager import SkillsManager
4
+
5
+ __all__ = ["SkillsManager"]
@@ -0,0 +1,276 @@
1
+ """Skill 生命周期管理器。
2
+
3
+ 提供 Skill 的安装、列表查询、状态检查、升级等核心业务逻辑。
4
+ 是 CLI 层(cli.py)和底层组件(detector/updater)之间的中间协调层。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import shutil
11
+ from pathlib import Path
12
+
13
+ from opscli.skills.discovery.detector import SkillDetector
14
+ from opscli.skills.domain.exceptions import error_to_dict
15
+ from opscli.skills.domain.models import (
16
+ SkillBatchInstallResult,
17
+ SkillBatchUpgradeResult,
18
+ SkillInstallResult,
19
+ SkillRecord,
20
+ SkillUpgradeResult,
21
+ runtime_to_tool_name,
22
+ )
23
+ from opscli.skills.sync.updater import SkillsUpdater
24
+
25
+
26
+ class SkillsManager:
27
+ """Skill 管理器,协调检测器和更新器完成 Skill 生命周期管理。"""
28
+
29
+ def __init__(self) -> None:
30
+ self.detector = SkillDetector() # 负责 Skill 发现
31
+ self.updater = SkillsUpdater() # 负责远端数据拉取和升级
32
+ self.templates_dir = Path(__file__).parent.parent / "templates" # 内置 Skill 模板目录
33
+
34
+ def list_templates(self) -> list[dict]:
35
+ """扫描内置模板目录,返回可安装的 Skill 列表。
36
+
37
+ 每项包含 name、version、description(从 SKILL.md 首段提取)。
38
+ """
39
+ templates: list[dict] = []
40
+ if not self.templates_dir.exists():
41
+ return templates
42
+ for skill_dir in sorted(p for p in self.templates_dir.iterdir() if p.is_dir()):
43
+ version_file = skill_dir / "data" / "VERSION.json"
44
+ if not version_file.exists():
45
+ continue
46
+ try:
47
+ payload = json.loads(version_file.read_text(encoding="utf-8"))
48
+ except Exception:
49
+ continue
50
+ version = str(payload.get("version", "unknown"))
51
+ description = self._extract_skill_description(skill_dir)
52
+ templates.append({"name": skill_dir.name, "version": version, "description": description})
53
+ return templates
54
+
55
+ def _extract_skill_description(self, skill_dir: Path) -> str:
56
+ """从 SKILL.md 提取 Skill 简介(标题后第一个非空行)。
57
+
58
+ 自动跳过 YAML frontmatter(--- 包裹区域)。
59
+ """
60
+ skill_md = skill_dir / "SKILL.md"
61
+ if not skill_md.exists():
62
+ return ""
63
+ try:
64
+ lines = skill_md.read_text(encoding="utf-8").splitlines()
65
+ except Exception:
66
+ return ""
67
+
68
+ # 跳过 YAML frontmatter(以 --- 开头和结尾的区域)
69
+ in_frontmatter = False
70
+ content_lines: list[str] = []
71
+ for line in lines:
72
+ stripped = line.strip()
73
+ if stripped == "---" and not content_lines:
74
+ in_frontmatter = not in_frontmatter
75
+ continue
76
+ if in_frontmatter:
77
+ continue
78
+ content_lines.append(line)
79
+
80
+ # 从正文内容中提取:跳过标题行(# 开头),取第一个非空的普通文本行
81
+ skip_title = True
82
+ for line in content_lines:
83
+ stripped = line.strip()
84
+ if skip_title and stripped.startswith("#"):
85
+ skip_title = False
86
+ continue
87
+ if stripped and not stripped.startswith("#") and not stripped.startswith(">"):
88
+ return stripped[:30] + ("…" if len(stripped) > 30 else "")
89
+ return ""
90
+
91
+ def list_skills(self, skills_dir: str | None = None, cwd: Path | None = None) -> list[SkillRecord]:
92
+ """列出所有已安装的 Skill,委托给 SkillDetector.discover()。"""
93
+ return self.detector.discover(skills_dir=skills_dir, cwd=cwd)
94
+
95
+ def install(
96
+ self,
97
+ skill_name: str,
98
+ *,
99
+ skills_dir: str | None = None,
100
+ cwd: Path | None = None,
101
+ runtime: str | list[str] | None = None,
102
+ force: bool = False,
103
+ ) -> SkillBatchInstallResult:
104
+ """从内置模板安装 Skill 到目标目录。
105
+
106
+ 流程:
107
+ 1. 检查模板是否存在(只支持内置 Skill)
108
+ 2. 确定安装目标路径(显式指定 或 自动检测)
109
+ 3. 若目标已存在且未指定 force,则报错
110
+ 4. 将模板目录整体复制到目标位置
111
+
112
+ Args:
113
+ skill_name: 要安装的 Skill 名称(如 "ops-dataset-query")
114
+ skills_dir: 显式指定安装根目录
115
+ cwd: 当前工作目录,用于自动检测安装目标
116
+ runtime: 运行时类型(claude/openclaw)
117
+ force: 是否覆盖已存在的安装
118
+ """
119
+ template_dir = self.templates_dir / skill_name
120
+ if not template_dir.exists():
121
+ raise ValueError(f"不支持的内置 Skill: {skill_name}")
122
+
123
+ current = cwd or Path.cwd()
124
+ if skills_dir:
125
+ targets = [(str(runtime or "custom"), Path(skills_dir).expanduser())]
126
+ else:
127
+ runtime_list = self._normalize_runtime_arg(runtime)
128
+ targets = self.detector.detect_install_targets(
129
+ cwd=current,
130
+ preferred_runtimes=runtime_list,
131
+ )
132
+
133
+ installs: list[SkillInstallResult] = []
134
+ for target_runtime, target_root in targets:
135
+ target_root.mkdir(parents=True, exist_ok=True)
136
+ target_dir = target_root / skill_name
137
+
138
+ replaced = False
139
+ if target_dir.exists():
140
+ if not force:
141
+ raise ValueError(f"Skill 已存在: {target_dir}")
142
+ shutil.rmtree(target_dir)
143
+ replaced = True
144
+
145
+ shutil.copytree(template_dir, target_dir)
146
+ installs.append(
147
+ SkillInstallResult(
148
+ name=skill_name,
149
+ runtime=target_runtime,
150
+ target_dir=target_dir,
151
+ version=self._read_version(target_dir),
152
+ replaced=replaced,
153
+ )
154
+ )
155
+
156
+ return SkillBatchInstallResult(name=skill_name, installs=installs)
157
+
158
+ def status(self, skills_dir: str | None = None, cwd: Path | None = None) -> dict:
159
+ """查询 Skill 安装状态,包含本地信息和远端版本对比。
160
+
161
+ 返回结构:
162
+ - installed: 本地已安装 Skill 列表(附带远端版本和是否有更新)
163
+ - remote_manifest: 远端 manifest 数据(可能为 None)
164
+ - remote_error: 远端请求失败时的错误信息
165
+ """
166
+ records = self.list_skills(skills_dir=skills_dir, cwd=cwd)
167
+
168
+ # 获取远端 manifest(仅支持 ops-dataset-query)
169
+ remote_manifest = None
170
+ remote_summary = None
171
+ remote_error = None
172
+ try:
173
+ remote_summary = self.updater.build_remote_summary("ops-dataset-query")
174
+ remote_manifest = remote_summary.get("manifest")
175
+ except Exception as exc:
176
+ remote_error = error_to_dict(exc)
177
+
178
+ remote_version = None
179
+ if remote_manifest:
180
+ remote_version = str(remote_manifest.get("version", "v0.0.0"))
181
+
182
+ # 为每个已安装 Skill 附带远端版本信息
183
+ enriched: list[dict] = []
184
+ for item in records:
185
+ row = item.to_dict()
186
+ if item.name == "ops-dataset-query" and remote_version:
187
+ row["remote_version"] = remote_version
188
+ row["has_update"] = self.updater.compare_versions(item.version, remote_version) < 0
189
+ else:
190
+ row["remote_version"] = None
191
+ row["has_update"] = False
192
+ enriched.append(row)
193
+
194
+ return {
195
+ "skills": self._summarize_installed_skills(enriched),
196
+ "installed": enriched,
197
+ "remote_manifest": remote_manifest,
198
+ "remote_summary": remote_summary,
199
+ "remote_error": remote_error,
200
+ }
201
+
202
+ def upgrade(
203
+ self,
204
+ *,
205
+ name: str = "ops-dataset-query",
206
+ skills_dir: str | None = None,
207
+ cwd: Path | None = None,
208
+ force: bool = False,
209
+ ) -> SkillBatchUpgradeResult:
210
+ """升级指定 Skill 到最新版本。
211
+
212
+ 当前仅支持 ops-dataset-query 的升级。会先查找本地已安装的记录,
213
+ 再委托给 SkillsUpdater 执行实际的远端数据拉取和文件替换。
214
+
215
+ Args:
216
+ name: 要升级的 Skill 名称
217
+ skills_dir: 指定扫描目录
218
+ cwd: 当前工作目录
219
+ force: 是否强制升级(即使版本号相同也重新拉取)
220
+ """
221
+ records = self.list_skills(skills_dir=skills_dir, cwd=cwd)
222
+ targets = [item for item in records if item.name == name]
223
+ if not targets:
224
+ raise ValueError(f"未找到已安装 Skill: {name}")
225
+ if name != "ops-dataset-query":
226
+ raise ValueError(f"暂不支持升级 Skill: {name}")
227
+
228
+ results = [
229
+ self.updater.upgrade_ops_dataset_query(target, force=force)
230
+ for target in targets
231
+ ]
232
+ return SkillBatchUpgradeResult(name=name, results=results)
233
+
234
+ def _normalize_runtime_arg(self, runtime: str | list[str] | None) -> list[str] | None:
235
+ """统一解析 runtime 参数。"""
236
+ if runtime is None:
237
+ return None
238
+ if isinstance(runtime, list):
239
+ return runtime
240
+ parts = [item.strip() for item in runtime.split(",") if item.strip()]
241
+ return parts or None
242
+
243
+ def _read_version(self, target_dir: Path) -> str:
244
+ """从安装目录的 data/VERSION.json 中读取版本号。"""
245
+ version_file = target_dir / "data" / "VERSION.json"
246
+ payload = json.loads(version_file.read_text(encoding="utf-8"))
247
+ return str(payload.get("version", "unknown"))
248
+
249
+ def _summarize_installed_skills(self, records: list[dict]) -> list[dict]:
250
+ """按 skill 名称聚合安装记录,生成稳定的对外 JSON 结构。"""
251
+ grouped: dict[str, dict] = {}
252
+ for item in records:
253
+ group = grouped.setdefault(
254
+ item["name"],
255
+ {
256
+ "name": item["name"],
257
+ "local_version": item["version"],
258
+ "remote_version": item["remote_version"],
259
+ "has_update": item["has_update"],
260
+ "installed_paths": [],
261
+ },
262
+ )
263
+
264
+ if item["version"] != group["local_version"]:
265
+ group["local_version"] = max(str(group["local_version"]), str(item["version"]))
266
+ if item["remote_version"] and not group["remote_version"]:
267
+ group["remote_version"] = item["remote_version"]
268
+ group["has_update"] = group["has_update"] or item["has_update"]
269
+ group["installed_paths"].append(
270
+ {
271
+ "tool": runtime_to_tool_name(item["runtime"]),
272
+ "path": item["root"],
273
+ }
274
+ )
275
+
276
+ return list(grouped.values())
@@ -0,0 +1,5 @@
1
+ """skills 同步层。"""
2
+
3
+ from opscli.skills.sync.updater import SkillsUpdater
4
+
5
+ __all__ = ["SkillsUpdater"]