Auto commit: 2026-03-28 16:17:35
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Skill upgrade flow extracted from the main CLI module."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
|
||||
def _as_dict(value: Any) -> Dict[str, Any]:
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _first_non_empty_string(obj: Dict[str, Any], keys: List[str]) -> str:
|
||||
for key in keys:
|
||||
value = obj.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_update_url(config: Dict[str, Any], skill_dir: Path, resolve_uri_with_base: Callable[[str, Path], str]) -> str:
|
||||
direct = _first_non_empty_string(
|
||||
config,
|
||||
["update_url", "updateUrl", "upgrade_url", "upgradeUrl", "manifest_url", "manifestUrl"],
|
||||
)
|
||||
if direct:
|
||||
return resolve_uri_with_base(direct, skill_dir)
|
||||
|
||||
for container_key in ("update", "upgrade", "autoupdate"):
|
||||
nested = _as_dict(config.get(container_key))
|
||||
url_value = _first_non_empty_string(nested, ["url", "uri", "manifest", "manifest_url"])
|
||||
if url_value:
|
||||
return resolve_uri_with_base(url_value, skill_dir)
|
||||
return ""
|
||||
|
||||
|
||||
def _read_installed_skill_version(
|
||||
skill_dir: Path,
|
||||
lock_meta: Dict[str, Any],
|
||||
skill_meta_name: str,
|
||||
) -> str:
|
||||
lock_version = _first_non_empty_string(lock_meta, ["version"])
|
||||
if lock_version:
|
||||
return lock_version
|
||||
|
||||
meta_path = skill_dir / skill_meta_name
|
||||
if meta_path.exists():
|
||||
try:
|
||||
raw = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
if isinstance(raw, dict):
|
||||
meta_version = _first_non_empty_string(raw, ["version"])
|
||||
if meta_version:
|
||||
return meta_version
|
||||
except json.JSONDecodeError:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def cmd_upgrade(args: Any, deps: Dict[str, Any]) -> int:
|
||||
load_lockfile = deps["load_lockfile"]
|
||||
save_lockfile = deps["save_lockfile"]
|
||||
read_json_from_uri = deps["read_json_from_uri"]
|
||||
extract_update_manifest_info = deps["extract_update_manifest_info"]
|
||||
resolve_uri_with_base = deps["resolve_uri_with_base"]
|
||||
version_is_newer = deps["version_is_newer"]
|
||||
install_zip_to_target = deps["install_zip_to_target"]
|
||||
skill_config_name = deps["skill_config_name"]
|
||||
skill_meta_name = deps["skill_meta_name"]
|
||||
|
||||
install_root = Path(args.dir).expanduser().resolve()
|
||||
lock = load_lockfile(install_root)
|
||||
skills = lock.get("skills", {})
|
||||
if not isinstance(skills, dict):
|
||||
skills = {}
|
||||
|
||||
if args.slug:
|
||||
targets = [args.slug]
|
||||
else:
|
||||
targets = sorted(skills.keys())
|
||||
if not targets:
|
||||
raise SystemExit(f"No installed skills in lockfile: {install_root / '.skills_store_lock.json'}")
|
||||
|
||||
checked = 0
|
||||
upgraded = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
for slug in targets:
|
||||
checked += 1
|
||||
target_dir = install_root / slug
|
||||
if not target_dir.exists():
|
||||
print(f"[{slug}] skip: skill directory not found: {target_dir}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
lock_meta = skills.get(slug)
|
||||
lock_meta_dict = lock_meta if isinstance(lock_meta, dict) else {}
|
||||
config_path = target_dir / skill_config_name
|
||||
if not config_path.exists():
|
||||
print(f"[{slug}] skip: {skill_config_name} not found")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
raw_config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"[{slug}] fail: invalid {skill_config_name}: {exc}")
|
||||
failed += 1
|
||||
continue
|
||||
if not isinstance(raw_config, dict):
|
||||
print(f"[{slug}] fail: {skill_config_name} must be a JSON object")
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
update_url = _extract_update_url(raw_config, target_dir, resolve_uri_with_base)
|
||||
if not update_url:
|
||||
print(f"[{slug}] skip: missing update URL in {skill_config_name}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
preserved_config_text = config_path.read_text(encoding="utf-8")
|
||||
manifest = read_json_from_uri(update_url, timeout=args.timeout)
|
||||
latest_version, package_uri, expected_sha = extract_update_manifest_info(manifest)
|
||||
if not latest_version:
|
||||
print(f"[{slug}] fail: update manifest missing version: {update_url}")
|
||||
failed += 1
|
||||
continue
|
||||
if not package_uri:
|
||||
print(f"[{slug}] fail: update manifest missing package URL: {update_url}")
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
current_version = _read_installed_skill_version(target_dir, lock_meta_dict, skill_meta_name)
|
||||
if not version_is_newer(latest_version, current_version):
|
||||
print(f"[{slug}] up-to-date: current={current_version or '<unknown>'} latest={latest_version}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
package_uri = resolve_uri_with_base(package_uri, target_dir)
|
||||
if args.check_only:
|
||||
print(
|
||||
f"[{slug}] upgrade available: current={current_version or '<unknown>'} "
|
||||
f"latest={latest_version} package={package_uri}"
|
||||
)
|
||||
continue
|
||||
|
||||
install_zip_to_target(
|
||||
slug=slug,
|
||||
zip_uri=package_uri,
|
||||
target_dir=target_dir,
|
||||
force=True,
|
||||
expected_sha256=expected_sha,
|
||||
)
|
||||
restored_config_path = target_dir / skill_config_name
|
||||
if not restored_config_path.exists():
|
||||
restored_config_path.write_text(preserved_config_text, encoding="utf-8")
|
||||
|
||||
updated_meta = dict(lock_meta_dict)
|
||||
updated_meta["zip_url"] = package_uri
|
||||
updated_meta["version"] = latest_version
|
||||
updated_meta["update_url"] = update_url
|
||||
if not updated_meta.get("name"):
|
||||
updated_meta["name"] = slug
|
||||
if not updated_meta.get("source"):
|
||||
updated_meta["source"] = "unknown"
|
||||
skills[slug] = updated_meta
|
||||
upgraded += 1
|
||||
print(f"[{slug}] upgraded: {current_version or '<unknown>'} -> {latest_version}")
|
||||
except SystemExit:
|
||||
failed += 1
|
||||
continue
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[{slug}] fail: {exc}")
|
||||
failed += 1
|
||||
|
||||
lock["skills"] = skills
|
||||
save_lockfile(install_root, lock)
|
||||
print(
|
||||
f"upgrade done: checked={checked} upgraded={upgraded} "
|
||||
f"skipped={skipped} failed={failed} dir={install_root}"
|
||||
)
|
||||
return 2 if failed > 0 else 0
|
||||
Reference in New Issue
Block a user