feat: auto-update pouch and catalog sources during normal use
Add taste-skill to catalog.yaml, and refresh pouch plus cloned catalog or custom Git sources on a 24h interval so users do not need a separate update command.
This commit is contained in:
+85
-16
@@ -58,6 +58,7 @@ from pouch.skills import (
|
||||
validate_skill_dir,
|
||||
validate_skill_name,
|
||||
)
|
||||
from pouch.auto_update import maybe_auto_update, record_auto_update, refresh_git_checkout, run_updates
|
||||
from pouch.sources import (
|
||||
discover_source_skills,
|
||||
fetch_source,
|
||||
@@ -250,15 +251,33 @@ def _collect_skill_names(positional: list[str] | None, flagged: list[str] | None
|
||||
return names
|
||||
|
||||
|
||||
def _ensure_source_fetched(name: str, source: str | None = None) -> None:
|
||||
def _ensure_source_fetched(
|
||||
name: str,
|
||||
source: str | None = None,
|
||||
*,
|
||||
refresh: bool = False,
|
||||
) -> None:
|
||||
name, source = split_skill_spec(name, source)
|
||||
catalog = load_catalog()
|
||||
sources = load_sources()
|
||||
if source in sources:
|
||||
root = source_skills_root(source, sources[source])
|
||||
entry = sources[source]
|
||||
root = source_skills_root(source, entry)
|
||||
if not root.is_dir():
|
||||
_print(f"拉取 source: {source}")
|
||||
fetch_source(source, sources[source])
|
||||
fetch_source(source, entry)
|
||||
elif refresh and not entry.get("local_path"):
|
||||
result = refresh_git_checkout(
|
||||
source_checkout(source, entry),
|
||||
str(entry.get("ref", "main") or "main"),
|
||||
name=f"custom:{source}",
|
||||
)
|
||||
if result.status == "updated":
|
||||
_err(f"[pouch] 已更新 custom:{source}")
|
||||
elif result.status == "failed":
|
||||
_err(
|
||||
f"[pouch] 更新 custom:{source} 失败,使用本地缓存: {result.detail}"
|
||||
)
|
||||
return
|
||||
|
||||
catalog_name = (
|
||||
@@ -274,7 +293,11 @@ def _ensure_source_fetched(name: str, source: str | None = None) -> None:
|
||||
source in (None, "catalog", catalog_name)
|
||||
or source == f"catalog:{catalog_name}"
|
||||
):
|
||||
_ensure_catalog_fetched(catalog_name, catalog[catalog_name])
|
||||
_ensure_catalog_fetched(
|
||||
catalog_name,
|
||||
catalog[catalog_name],
|
||||
refresh=refresh,
|
||||
)
|
||||
return
|
||||
|
||||
if source not in (None, "catalog"):
|
||||
@@ -283,17 +306,31 @@ def _ensure_source_fetched(name: str, source: str | None = None) -> None:
|
||||
if name not in catalog:
|
||||
return
|
||||
|
||||
_ensure_catalog_fetched(name, catalog[name])
|
||||
_ensure_catalog_fetched(name, catalog[name], refresh=refresh)
|
||||
|
||||
|
||||
def _ensure_catalog_fetched(name: str, entry: dict[str, object]) -> None:
|
||||
def _ensure_catalog_fetched(
|
||||
name: str,
|
||||
entry: dict[str, object],
|
||||
*,
|
||||
refresh: bool = False,
|
||||
) -> None:
|
||||
path = catalog_skill_path(name, entry)
|
||||
repo = catalog_repo(entry)
|
||||
ref = str(entry.get("ref", "main") or "main")
|
||||
dest = catalog_checkout_path(name, entry)
|
||||
if dest.is_dir() and (dest / ".git").exists() and refresh and not str(entry.get("repo", "")).startswith("~"):
|
||||
result = refresh_git_checkout(dest, ref, name=f"catalog:{name}")
|
||||
if result.status == "updated":
|
||||
_err(f"[pouch] 已更新 catalog:{name}")
|
||||
elif result.status == "failed":
|
||||
_err(f"[pouch] 更新 catalog:{name} 失败,使用本地缓存: {result.detail}")
|
||||
if (path / "SKILL.md").is_file() or discover_catalog_skills(name, entry):
|
||||
return
|
||||
|
||||
if (path / "SKILL.md").is_file() or discover_catalog_skills(name, entry):
|
||||
return
|
||||
|
||||
repo = catalog_repo(entry)
|
||||
ref = entry.get("ref", "main")
|
||||
dest = catalog_checkout_path(name, entry)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dest.exists():
|
||||
raise SystemExit(
|
||||
@@ -334,14 +371,17 @@ def _expand_install_request(
|
||||
catalog = load_catalog()
|
||||
sources = load_sources()
|
||||
if source in sources:
|
||||
_ensure_source_fetched(name, source, refresh=True)
|
||||
return [(name, source)]
|
||||
if source == "catalog" and name in catalog:
|
||||
_ensure_catalog_fetched(name, catalog[name], refresh=True)
|
||||
available = _catalog_skill_names(name, catalog[name])
|
||||
root = catalog_skill_path(name, catalog[name])
|
||||
if (root / "SKILL.md").is_file():
|
||||
return [(name, "catalog")]
|
||||
return [(skill_name, f"catalog:{name}") for skill_name in available]
|
||||
if source in catalog:
|
||||
_ensure_catalog_fetched(source, catalog[source], refresh=True)
|
||||
available = _catalog_skill_names(source, catalog[source])
|
||||
if name not in available:
|
||||
raise SystemExit(
|
||||
@@ -349,13 +389,14 @@ def _expand_install_request(
|
||||
)
|
||||
return [(name, f"catalog:{source}")]
|
||||
if source is None and name in catalog:
|
||||
_ensure_catalog_fetched(name, catalog[name], refresh=True)
|
||||
available = _catalog_skill_names(name, catalog[name])
|
||||
root = catalog_skill_path(name, catalog[name])
|
||||
if (root / "SKILL.md").is_file():
|
||||
return [(name, f"catalog:{name}")]
|
||||
return [(skill_name, f"catalog:{name}") for skill_name in available]
|
||||
if source is None and name in sources:
|
||||
_ensure_source_fetched(name, name)
|
||||
_ensure_source_fetched(name, name, refresh=True)
|
||||
available = discover_source_skills(name, sources[name])
|
||||
if not available:
|
||||
raise SystemExit(f"custom source {name!r} 中没有可安装的 skill")
|
||||
@@ -375,7 +416,7 @@ def _install_skill(
|
||||
source: str | None = None,
|
||||
) -> str:
|
||||
name, source = split_skill_spec(name, source)
|
||||
_ensure_source_fetched(name, source)
|
||||
_ensure_source_fetched(name, source, refresh=True)
|
||||
skill_path, resolved_source = resolve_skill_source(name, source=source)
|
||||
links = [
|
||||
(target, agent_skill_dir(target, project_root=project_root) / name)
|
||||
@@ -540,7 +581,23 @@ def cmd_update(args: argparse.Namespace) -> None:
|
||||
del args
|
||||
ensure_pouch_home()
|
||||
_print(f"更新 pouch: {POUCH_HOME}")
|
||||
subprocess.run(["git", "-C", str(POUCH_HOME), "pull"], check=True)
|
||||
results = run_updates()
|
||||
record_auto_update()
|
||||
labels = {
|
||||
"updated": "已更新",
|
||||
"current": "已是最新",
|
||||
"skipped": "跳过",
|
||||
"failed": "失败",
|
||||
}
|
||||
for item in results:
|
||||
extra = f"({item.detail})" if item.detail else ""
|
||||
line = f"{labels.get(item.status, item.status)} {item.name}{extra}"
|
||||
if item.status == "failed":
|
||||
_err(line)
|
||||
else:
|
||||
_print(line)
|
||||
if any(item.status == "failed" for item in results):
|
||||
raise SystemExit("部分来源更新失败")
|
||||
|
||||
|
||||
def _installed_links(
|
||||
@@ -640,7 +697,7 @@ def cmd_add(args: argparse.Namespace) -> None:
|
||||
elif source == "catalog":
|
||||
names = list(load_catalog())
|
||||
else:
|
||||
_ensure_source_fetched("", source)
|
||||
_ensure_source_fetched("", source, refresh=True)
|
||||
names = list_custom_skills(source)[source]
|
||||
else:
|
||||
names = list_builtin_skills()
|
||||
@@ -662,7 +719,7 @@ def cmd_add(args: argparse.Namespace) -> None:
|
||||
original_links: list[tuple[Path, str | None]] = []
|
||||
for skill_name, source in requests:
|
||||
validate_skill_name(skill_name)
|
||||
_ensure_source_fetched(skill_name, source)
|
||||
_ensure_source_fetched(skill_name, source, refresh=True)
|
||||
resolve_skill_source(skill_name, source=source)
|
||||
for target in targets:
|
||||
link = agent_skill_dir(target, project_root=project_root) / skill_name
|
||||
@@ -1122,7 +1179,7 @@ def cmd_sync(args: argparse.Namespace) -> None:
|
||||
for entry in iter_manifest_skills(data):
|
||||
name = entry["name"]
|
||||
source = entry.get("source")
|
||||
_ensure_source_fetched(name, source)
|
||||
_ensure_source_fetched(name, source, refresh=True)
|
||||
skill_path, _ = resolve_manifest_skill(entry)
|
||||
skill_targets = targets
|
||||
if entry.get("targets"):
|
||||
@@ -1702,6 +1759,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
description="自研 Agent Skills 安装与管理 CLI(接口对齐 Vercel skills)",
|
||||
)
|
||||
parser.add_argument("--version", action="version", version=f"pouch {__version__}")
|
||||
parser.add_argument(
|
||||
"--no-auto-update",
|
||||
action="store_true",
|
||||
help="本次跳过自动更新 pouch 与已拉取的 source",
|
||||
)
|
||||
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
@@ -1711,7 +1773,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
p_bootstrap.set_defaults(func=cmd_bootstrap)
|
||||
|
||||
p_update = sub.add_parser("update", help="通过 git pull 更新 pouch 自身")
|
||||
p_update = sub.add_parser(
|
||||
"update",
|
||||
help="更新 pouch 自身以及已拉取的 catalog / custom source",
|
||||
)
|
||||
p_update.set_defaults(func=cmd_update)
|
||||
|
||||
p_list = sub.add_parser("list", help="列出所有 source 中的 skill")
|
||||
@@ -1845,6 +1910,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
maybe_auto_update(
|
||||
command=getattr(args, "command", None),
|
||||
disabled=bool(getattr(args, "no_auto_update", False)),
|
||||
)
|
||||
args.func(args)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user