feat: add self-update and improve skill selector

This commit is contained in:
2026-07-29 10:29:48 +08:00
parent afbdde157f
commit 18ebb70911
9 changed files with 431 additions and 47 deletions
+82 -24
View File
@@ -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)