feat: sap-cli skill v2.1.0 — self-contained distributable package

- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types)
- references/: tool constraints + error handling (self-contained)
- scripts/setup.py: one-click install/config/verify
- SKILL.md: full command reference + dual-platform install guide
- VERSION: 2.1.0

Built from D:/Codespace/sap-cli via scripts/pack_skill.py
This commit is contained in:
2026-06-13 20:52:24 +08:00
commit 1e39b6da88
49 changed files with 7172 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
"""依赖排序模块。
使用 Kahn 算法对清单对象进行拓扑排序,
按 depends_on 显式依赖和 TYPE_PRIORITY 类型优先级决定执行顺序。
"""
from __future__ import annotations
import logging
from collections import deque
from sapcli.exceptions import CyclicDependencyError
from sapcli.manifest import ManifestEntry
logger = logging.getLogger("sapcli.sorter")
# 类型默认优先级(数值越小越先执行)
TYPE_PRIORITY: dict[str, int] = {
"domain": 10,
"dataelement": 20,
"structure": 25,
"table": 30,
"tabletype": 40,
"view": 42,
"lockobject": 44,
"searchhelp": 46,
"messageclass": 48,
"interface": 50,
"class": 60,
"functiongroup": 70,
"function": 75,
"include": 78,
"cdsview": 79,
"report": 80,
}
# 未注册类型的默认优先级
_DEFAULT_PRIORITY = 90
def topological_sort(objects: list[ManifestEntry]) -> list[ManifestEntry]:
"""对清单对象进行拓扑排序。
1. 按 depends_on 建有向边(被依赖 → 当前对象)
2. 无入边的节点按 TYPE_PRIORITY 排序作为 tie-breaker
3. 检测循环依赖 → 抛出 CyclicDependencyError
Returns:
排序后的对象列表(依赖在前,被依赖在后...不,应该是依赖在前,被依赖的对象先执行)。
"""
if not objects:
return []
name_map: dict[str, ManifestEntry] = {obj.name: obj for obj in objects}
names = set(name_map.keys())
# ── 建图:计算入度 ──
in_degree: dict[str, int] = {name: 0 for name in names}
# adj[A] = [B, C, ...] 表示 A 完成后可以释放 B、C 的入度
adj: dict[str, list[str]] = {name: [] for name in names}
for obj in objects:
for dep in obj.depends_on:
if dep not in names:
# 依赖不在当前列表中,忽略(外部依赖)
logger.debug("忽略外部依赖: %s%s", obj.name, dep)
continue
# dep → objdep 先执行,obj 后执行)
adj[dep].append(obj.name)
in_degree[obj.name] += 1
# ── Kahn 算法 ──
# 初始化:入度为 0 的节点
ready: list[str] = [
name for name, deg in in_degree.items() if deg == 0
]
# 按 TYPE_PRIORITY 排序
ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
queue: deque[str] = deque(ready)
sorted_names: list[str] = []
while queue:
# 取出优先级最高(数值最小)的节点
# 由于 deque 不支持按优先级取,先排序再取
# 实际实现:每次从 ready 列表中取第一个
current = queue.popleft()
sorted_names.append(current)
# 释放后续节点
newly_ready: list[str] = []
for neighbor in adj[current]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
newly_ready.append(neighbor)
# 新就绪的节点按优先级排序后加入队列
newly_ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
queue.extend(newly_ready)
# ── 循环检测 ──
if len(sorted_names) != len(names):
remaining = names - set(sorted_names)
# 尝试找出循环链
cycle = _detect_cycle(remaining, adj, in_degree)
raise CyclicDependencyError(cycle)
result = [name_map[n] for n in sorted_names]
logger.info("拓扑排序完成: %d 个对象", len(result))
return result
def _priority(*, n_map: dict[str, ManifestEntry], n: str) -> int:
"""获取节点优先级数值。"""
obj_type = n_map[n].type if n in n_map else ""
return TYPE_PRIORITY.get(obj_type, _DEFAULT_PRIORITY)
def _detect_cycle(
remaining: set[str],
adj: dict[str, list[str]],
in_degree: dict[str, int],
) -> list[str]:
"""尝试在剩余节点中检测循环,返回循环中的节点名列表。"""
# 沿着入度 > 0 的边走,迟早会回到已访问的节点
if not remaining:
return []
visited: set[str] = set()
path: list[str] = []
start = next(iter(remaining))
current = start
for _ in range(len(remaining) + 1):
if current in visited:
# 找到循环起点
idx = path.index(current)
return path[idx:]
visited.add(current)
path.append(current)
# 找下一个仍在 remaining 中的后继
found_next = False
for neighbor in adj.get(current, []):
if neighbor in remaining:
current = neighbor
found_next = True
break
if not found_next:
break
# 无法确定精确循环链,返回所有剩余节点
return list(remaining)