feat: add self-update and improve skill selector
This commit is contained in:
+82
-24
@@ -38,6 +38,7 @@ from skiff.registry import (
|
||||
external_checkout_path,
|
||||
external_skill_path,
|
||||
load_registry,
|
||||
registry_repo,
|
||||
save_registry,
|
||||
)
|
||||
from skiff.selector import SkillChoice, select_skills
|
||||
@@ -139,7 +140,7 @@ def _ensure_registry_fetched(name: str, entry: dict[str, object]) -> None:
|
||||
if (path / "SKILL.md").is_file() or discover_external_skills(name, entry):
|
||||
return
|
||||
|
||||
repo = str(entry["repo"])
|
||||
repo = registry_repo(entry)
|
||||
ref = entry.get("ref", "main")
|
||||
dest = external_checkout_path(name, entry)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -286,6 +287,29 @@ def _is_fully_installed(
|
||||
)
|
||||
|
||||
|
||||
def _global_installation_note(
|
||||
name: str,
|
||||
expected: Path,
|
||||
targets: list[str],
|
||||
) -> str:
|
||||
installed: list[str] = []
|
||||
conflicts: list[str] = []
|
||||
for target in targets:
|
||||
link = agent_skill_dir(target) / name
|
||||
status = check_link(link, expected)
|
||||
if status.ok:
|
||||
installed.append(target)
|
||||
elif link.exists() or link.is_symlink():
|
||||
conflicts.append(target)
|
||||
|
||||
parts: list[str] = []
|
||||
if installed:
|
||||
parts.append(f"全局: {','.join(installed)}")
|
||||
if conflicts:
|
||||
parts.append(f"全局同名冲突: {','.join(conflicts)}")
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
def _remove_skill(
|
||||
name: str,
|
||||
targets: list[str],
|
||||
@@ -346,6 +370,13 @@ def cmd_bootstrap(args: argparse.Namespace) -> None:
|
||||
_print("已安装项目 skill 到所有 agent")
|
||||
|
||||
|
||||
def cmd_update(args: argparse.Namespace) -> None:
|
||||
del args
|
||||
ensure_skills_home()
|
||||
_print(f"更新 skiff: {SKILLS_HOME}")
|
||||
subprocess.run(["git", "-C", str(SKILLS_HOME), "pull"], check=True)
|
||||
|
||||
|
||||
def _installed_links(
|
||||
name: str,
|
||||
targets: list[str],
|
||||
@@ -525,17 +556,38 @@ def cmd_select(args: argparse.Namespace) -> None:
|
||||
for name in [*owned_names, *registry]:
|
||||
validate_skill_name(name)
|
||||
|
||||
choices = [
|
||||
SkillChoice(
|
||||
def make_choice(
|
||||
*,
|
||||
name: str,
|
||||
installed_name: str,
|
||||
expected: Path,
|
||||
kind: str,
|
||||
description: str,
|
||||
) -> SkillChoice:
|
||||
return SkillChoice(
|
||||
name=name,
|
||||
kind="owned",
|
||||
description=skill_description(name) or "",
|
||||
kind=kind,
|
||||
description=description,
|
||||
installed=_is_fully_installed(
|
||||
name,
|
||||
SKILLS_DIR / name,
|
||||
installed_name,
|
||||
expected,
|
||||
project_root,
|
||||
targets,
|
||||
),
|
||||
readonly_status=(
|
||||
_global_installation_note(installed_name, expected, targets)
|
||||
if project_root is not None
|
||||
else ""
|
||||
),
|
||||
)
|
||||
|
||||
choices = [
|
||||
make_choice(
|
||||
name=name,
|
||||
installed_name=name,
|
||||
expected=SKILLS_DIR / name,
|
||||
kind="owned",
|
||||
description=skill_description(name) or "",
|
||||
)
|
||||
for name in owned_names
|
||||
]
|
||||
@@ -550,40 +602,43 @@ def cmd_select(args: argparse.Namespace) -> None:
|
||||
_err(f"警告: registry 条目与 owned skill 同名,已忽略 external: {package}")
|
||||
continue
|
||||
choices.append(
|
||||
SkillChoice(
|
||||
make_choice(
|
||||
name=package,
|
||||
installed_name=package,
|
||||
expected=root,
|
||||
kind="external",
|
||||
description=str(entry.get("description", "")),
|
||||
installed=_is_fully_installed(
|
||||
package,
|
||||
root,
|
||||
project_root,
|
||||
targets,
|
||||
),
|
||||
)
|
||||
)
|
||||
choice_requests[package] = (package, "registry")
|
||||
continue
|
||||
for skill_name in skill_names:
|
||||
expected = root / skill_name
|
||||
if (
|
||||
skill_name in owned_names
|
||||
and expected.resolve() == (SKILLS_DIR / skill_name).resolve()
|
||||
):
|
||||
continue
|
||||
choice_name = f"{package}/{skill_name}"
|
||||
description = read_skill_meta(root / skill_name).get("description", "")
|
||||
description = read_skill_meta(expected).get("description", "")
|
||||
choices.append(
|
||||
SkillChoice(
|
||||
make_choice(
|
||||
name=choice_name,
|
||||
installed_name=skill_name,
|
||||
expected=expected,
|
||||
kind=f"external:{package}",
|
||||
description=description,
|
||||
installed=_is_fully_installed(
|
||||
skill_name,
|
||||
root / skill_name,
|
||||
project_root,
|
||||
targets,
|
||||
),
|
||||
)
|
||||
)
|
||||
choice_requests[choice_name] = (skill_name, f"registry:{package}")
|
||||
|
||||
try:
|
||||
selected = select_skills(choices)
|
||||
scope_label = (
|
||||
"全局"
|
||||
if project_root is None
|
||||
else f"项目 {project_root}(全局状态只读)"
|
||||
)
|
||||
selected = select_skills(choices, scope_label=scope_label)
|
||||
except (RuntimeError, OSError) as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
if selected is None:
|
||||
@@ -738,7 +793,7 @@ def cmd_fetch(args: argparse.Namespace) -> None:
|
||||
raise SystemExit(f"registry 中不存在: {args.name}")
|
||||
|
||||
entry = registry[args.name]
|
||||
repo = entry["repo"]
|
||||
repo = registry_repo(entry)
|
||||
ref = entry.get("ref", "main")
|
||||
dest = external_checkout_path(args.name, entry)
|
||||
|
||||
@@ -1149,6 +1204,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
p_bootstrap.set_defaults(func=cmd_bootstrap)
|
||||
|
||||
p_update = sub.add_parser("update", help="通过 git pull 更新 skiff 自身")
|
||||
p_update.set_defaults(func=cmd_update)
|
||||
|
||||
p_list = sub.add_parser("list", help="列出所有 source 中的 skill")
|
||||
p_list.add_argument("--source", help="只列出指定来源(owned、registry 或 custom source)")
|
||||
p_list.set_defaults(func=cmd_list)
|
||||
|
||||
Reference in New Issue
Block a user