refactor: unify skill source model
This commit is contained in:
+238
-253
@@ -15,7 +15,7 @@ from skiff.gitops import publish as git_publish
|
||||
from skiff.paths import (
|
||||
ALL_TARGETS,
|
||||
DRAFTS_DIR,
|
||||
EXTERNALS_DIR,
|
||||
CATALOG_CACHE_DIR,
|
||||
CONFIG_FILE,
|
||||
SKILLS_DIR,
|
||||
SKILLS_HOME,
|
||||
@@ -29,22 +29,22 @@ from skiff.project import (
|
||||
load_manifest,
|
||||
remove_skill_from_manifest,
|
||||
resolve_manifest_skill,
|
||||
save_manifest,
|
||||
)
|
||||
from skiff.registry import (
|
||||
discover_external_skills,
|
||||
external_checkout_path,
|
||||
external_skill_path,
|
||||
load_registry,
|
||||
registry_repo,
|
||||
save_registry,
|
||||
from skiff.catalog import (
|
||||
discover_catalog_skills,
|
||||
catalog_checkout_path,
|
||||
catalog_skill_path,
|
||||
load_catalog,
|
||||
catalog_repo,
|
||||
save_catalog,
|
||||
)
|
||||
from skiff.selector import SkillChoice, select_skills
|
||||
from skiff.skills import (
|
||||
list_custom_skills,
|
||||
list_owned_skills,
|
||||
owned_skill_path,
|
||||
list_builtin_skills,
|
||||
builtin_skill_path,
|
||||
read_skill_meta,
|
||||
normalize_source,
|
||||
resolve_skill_source,
|
||||
split_skill_spec,
|
||||
skill_description,
|
||||
@@ -52,6 +52,7 @@ from skiff.skills import (
|
||||
validate_skill_name,
|
||||
)
|
||||
from skiff.sources import (
|
||||
discover_source_skills,
|
||||
fetch_source,
|
||||
load_sources,
|
||||
save_sources,
|
||||
@@ -71,10 +72,6 @@ def _err(msg: str) -> None:
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
def _warn_deprecated(old: str, new: str) -> None:
|
||||
_err(f"警告: `{old}` 已弃用,请改用 `{new}`")
|
||||
|
||||
|
||||
def _project_root(explicit: str | None = None) -> Path:
|
||||
if explicit:
|
||||
return Path(explicit).resolve()
|
||||
@@ -93,13 +90,13 @@ def _collect_skill_names(positional: list[str] | None, flagged: list[str] | None
|
||||
if flagged:
|
||||
names.extend(flagged)
|
||||
if "*" in names:
|
||||
return list_owned_skills()
|
||||
return list_builtin_skills()
|
||||
return names
|
||||
|
||||
|
||||
def _ensure_source_fetched(name: str, source: str | None = None) -> None:
|
||||
name, source = split_skill_spec(name, source)
|
||||
registry = load_registry()
|
||||
catalog = load_catalog()
|
||||
sources = load_sources()
|
||||
if source in sources:
|
||||
root = source_skills_root(source, sources[source])
|
||||
@@ -108,68 +105,68 @@ def _ensure_source_fetched(name: str, source: str | None = None) -> None:
|
||||
fetch_source(source, sources[source])
|
||||
return
|
||||
|
||||
registry_name = (
|
||||
catalog_name = (
|
||||
source.split(":", 1)[1]
|
||||
if source and source.startswith("registry:")
|
||||
if source and source.startswith("catalog:")
|
||||
else source
|
||||
if source in registry
|
||||
if source in catalog
|
||||
else name
|
||||
if name in registry
|
||||
if name in catalog
|
||||
else None
|
||||
)
|
||||
if registry_name and (
|
||||
source in (None, "registry", registry_name)
|
||||
or source == f"registry:{registry_name}"
|
||||
if catalog_name and (
|
||||
source in (None, "catalog", catalog_name)
|
||||
or source == f"catalog:{catalog_name}"
|
||||
):
|
||||
_ensure_registry_fetched(registry_name, registry[registry_name])
|
||||
_ensure_catalog_fetched(catalog_name, catalog[catalog_name])
|
||||
return
|
||||
|
||||
if source not in (None, "registry"):
|
||||
if source not in (None, "catalog"):
|
||||
return
|
||||
|
||||
if name not in registry:
|
||||
if name not in catalog:
|
||||
return
|
||||
|
||||
_ensure_registry_fetched(name, registry[name])
|
||||
_ensure_catalog_fetched(name, catalog[name])
|
||||
|
||||
|
||||
def _ensure_registry_fetched(name: str, entry: dict[str, object]) -> None:
|
||||
path = external_skill_path(name, entry)
|
||||
if (path / "SKILL.md").is_file() or discover_external_skills(name, entry):
|
||||
def _ensure_catalog_fetched(name: str, entry: dict[str, object]) -> None:
|
||||
path = catalog_skill_path(name, entry)
|
||||
if (path / "SKILL.md").is_file() or discover_catalog_skills(name, entry):
|
||||
return
|
||||
|
||||
repo = registry_repo(entry)
|
||||
repo = catalog_repo(entry)
|
||||
ref = entry.get("ref", "main")
|
||||
dest = external_checkout_path(name, entry)
|
||||
dest = catalog_checkout_path(name, entry)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dest.exists():
|
||||
raise SystemExit(
|
||||
f"外部仓库已存在但 skill 路径无效: {external_skill_path(name, entry)}"
|
||||
f"外部仓库已存在但 skill 路径无效: {catalog_skill_path(name, entry)}"
|
||||
)
|
||||
_print(f"拉取外部 skill: {name}")
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", "--branch", ref, "--", repo, str(dest)],
|
||||
check=True,
|
||||
)
|
||||
if not discover_external_skills(name, entry):
|
||||
if not discover_catalog_skills(name, entry):
|
||||
raise SystemExit(
|
||||
f"registry 条目 {name!r} 的 path 中没有可安装的 SKILL.md: {path}"
|
||||
f"catalog 条目 {name!r} 的 path 中没有可安装的 SKILL.md: {path}"
|
||||
)
|
||||
|
||||
|
||||
def _registry_skill_names(
|
||||
def _catalog_skill_names(
|
||||
name: str,
|
||||
entry: dict[str, object] | None = None,
|
||||
) -> list[str]:
|
||||
entry = entry or load_registry().get(name)
|
||||
entry = entry or load_catalog().get(name)
|
||||
if not entry:
|
||||
raise SystemExit(f"registry 中不存在: {name}")
|
||||
_ensure_registry_fetched(name, entry)
|
||||
names = list(discover_external_skills(name, entry))
|
||||
raise SystemExit(f"catalog 中不存在: {name}")
|
||||
_ensure_catalog_fetched(name, entry)
|
||||
names = list(discover_catalog_skills(name, entry))
|
||||
for skill_name in names:
|
||||
validate_skill_name(skill_name)
|
||||
if not names:
|
||||
raise SystemExit(f"registry 条目 {name!r} 中没有可安装的 skill")
|
||||
raise SystemExit(f"catalog 条目 {name!r} 中没有可安装的 skill")
|
||||
return names
|
||||
|
||||
|
||||
@@ -178,34 +175,39 @@ def _expand_install_request(
|
||||
explicit_source: str | None = None,
|
||||
) -> list[tuple[str, str | None]]:
|
||||
name, source = split_skill_spec(spec, explicit_source)
|
||||
registry = load_registry()
|
||||
if source in load_sources():
|
||||
catalog = load_catalog()
|
||||
sources = load_sources()
|
||||
if source in sources:
|
||||
return [(name, source)]
|
||||
if source == "registry" and name in registry:
|
||||
available = _registry_skill_names(name, registry[name])
|
||||
root = external_skill_path(name, registry[name])
|
||||
if source == "catalog" and name in catalog:
|
||||
available = _catalog_skill_names(name, catalog[name])
|
||||
root = catalog_skill_path(name, catalog[name])
|
||||
if (root / "SKILL.md").is_file():
|
||||
return [(name, "registry")]
|
||||
return [(skill_name, f"registry:{name}") for skill_name in available]
|
||||
if source in registry:
|
||||
available = _registry_skill_names(source, registry[source])
|
||||
return [(name, "catalog")]
|
||||
return [(skill_name, f"catalog:{name}") for skill_name in available]
|
||||
if source in catalog:
|
||||
available = _catalog_skill_names(source, catalog[source])
|
||||
if name not in available:
|
||||
raise SystemExit(
|
||||
f"registry collection {source!r} 中找不到 skill {name!r}"
|
||||
f"catalog collection {source!r} 中找不到 skill {name!r}"
|
||||
)
|
||||
return [(name, f"registry:{source}")]
|
||||
if source is None and name in registry:
|
||||
available = _registry_skill_names(name, registry[name])
|
||||
root = external_skill_path(name, registry[name])
|
||||
return [(name, f"catalog:{source}")]
|
||||
if source is None and name in catalog:
|
||||
available = _catalog_skill_names(name, catalog[name])
|
||||
root = catalog_skill_path(name, catalog[name])
|
||||
if (root / "SKILL.md").is_file():
|
||||
return [(name, "registry")]
|
||||
return [(skill_name, f"registry:{name}") for skill_name in available]
|
||||
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)
|
||||
available = discover_source_skills(name, sources[name])
|
||||
if not available:
|
||||
raise SystemExit(f"custom source {name!r} 中没有可安装的 skill")
|
||||
return [(skill_name, name) for skill_name in available]
|
||||
return [(name, source)]
|
||||
|
||||
|
||||
def _manifest_source_details(resolved_source: str) -> tuple[str, dict[str, object]]:
|
||||
if resolved_source.startswith("registry:"):
|
||||
return "registry", {"registry": resolved_source.split(":", 1)[1]}
|
||||
return resolved_source, {}
|
||||
|
||||
|
||||
@@ -329,27 +331,37 @@ def _remove_skill(
|
||||
|
||||
def cmd_list(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
owned = list_owned_skills()
|
||||
registry = load_registry()
|
||||
source_filter = normalize_source(args.source)
|
||||
builtin = list_builtin_skills()
|
||||
catalog = load_catalog()
|
||||
custom = (
|
||||
{}
|
||||
if args.source in ("owned", "registry")
|
||||
else list_custom_skills(args.source)
|
||||
if source_filter in ("builtin", "catalog")
|
||||
or (source_filter and source_filter.startswith("catalog:"))
|
||||
else list_custom_skills(source_filter)
|
||||
)
|
||||
|
||||
if args.source in (None, "owned"):
|
||||
_print("自研 (owned):")
|
||||
for name in owned:
|
||||
if source_filter in (None, "builtin"):
|
||||
_print("内置 (builtin):")
|
||||
for name in builtin:
|
||||
_print(f" {name}")
|
||||
|
||||
if args.source in (None, "registry"):
|
||||
_print("\n外部 (registry):")
|
||||
if not registry:
|
||||
if source_filter in (None, "catalog"):
|
||||
_print("\n目录 (catalog):")
|
||||
if not catalog:
|
||||
_print(" (无)")
|
||||
else:
|
||||
for name, entry in registry.items():
|
||||
for name, entry in catalog.items():
|
||||
repo = entry.get("repo", "?")
|
||||
_print(f" {name} ({repo})")
|
||||
elif source_filter and source_filter.startswith("catalog:"):
|
||||
provider = source_filter.split(":", 1)[1]
|
||||
if provider not in catalog:
|
||||
raise SystemExit(f"catalog 中不存在: {provider}")
|
||||
_ensure_catalog_fetched(provider, catalog[provider])
|
||||
_print(f"目录 (catalog:{provider}):")
|
||||
for name in discover_catalog_skills(provider, catalog[provider]):
|
||||
_print(f" {name}")
|
||||
|
||||
for source, names in custom.items():
|
||||
_print(f"\n自定义 ({source}):")
|
||||
@@ -363,9 +375,9 @@ def cmd_bootstrap(args: argparse.Namespace) -> None:
|
||||
del args
|
||||
ensure_skills_home()
|
||||
project_skill = "skiff"
|
||||
owned_skill_path(project_skill)
|
||||
builtin_skill_path(project_skill)
|
||||
_install_skill(project_skill, list(ALL_TARGETS), project_root=None)
|
||||
_print("已安装项目 skill 到所有 agent")
|
||||
_print("已全局安装 builtin skiff skill 到所有 agent")
|
||||
|
||||
|
||||
def cmd_update(args: argparse.Namespace) -> None:
|
||||
@@ -393,20 +405,20 @@ def _installed_links(
|
||||
def cmd_status(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||
owned = list_owned_skills()
|
||||
registry = load_registry()
|
||||
builtin = list_builtin_skills()
|
||||
catalog = load_catalog()
|
||||
custom = list_custom_skills()
|
||||
entries = [("owned", name) for name in owned]
|
||||
unfetched_registry: list[str] = []
|
||||
for package, entry in registry.items():
|
||||
discovered = discover_external_skills(package, entry)
|
||||
entries = [("builtin", name) for name in builtin]
|
||||
unfetched_catalog: list[str] = []
|
||||
for package, entry in catalog.items():
|
||||
discovered = discover_catalog_skills(package, entry)
|
||||
if not discovered:
|
||||
unfetched_registry.append(package)
|
||||
elif (external_skill_path(package, entry) / "SKILL.md").is_file():
|
||||
entries.append(("registry", package))
|
||||
unfetched_catalog.append(package)
|
||||
elif (catalog_skill_path(package, entry) / "SKILL.md").is_file():
|
||||
entries.append((f"catalog:{package}", package))
|
||||
else:
|
||||
entries.extend(
|
||||
(f"registry:{package}", skill_name)
|
||||
(f"catalog:{package}", skill_name)
|
||||
for skill_name in discovered
|
||||
)
|
||||
entries.extend((source, name) for source, names in custom.items() for name in names)
|
||||
@@ -414,8 +426,8 @@ def cmd_status(args: argparse.Namespace) -> None:
|
||||
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
||||
_print(f"agents: {', '.join(targets)}\n")
|
||||
|
||||
for package in unfetched_registry:
|
||||
_print(f"[registry] {package}\n (未 fetch)\n")
|
||||
for package in unfetched_catalog:
|
||||
_print(f"[catalog] {package}\n (未 fetch)\n")
|
||||
|
||||
for source, name in entries:
|
||||
_print(f"[{source}] {name}")
|
||||
@@ -438,13 +450,13 @@ def cmd_status(args: argparse.Namespace) -> None:
|
||||
|
||||
def _print_available_skills() -> None:
|
||||
ensure_skills_home()
|
||||
owned = list_owned_skills()
|
||||
if not owned:
|
||||
builtin = list_builtin_skills()
|
||||
if not builtin:
|
||||
_print("~/.skills/skills/ 中没有自研 skill")
|
||||
return
|
||||
|
||||
_print(f"来源: {SKILLS_DIR}\n")
|
||||
for name in owned:
|
||||
for name in builtin:
|
||||
desc = skill_description(name)
|
||||
_print(f" {name}")
|
||||
if desc:
|
||||
@@ -466,15 +478,16 @@ def cmd_add(args: argparse.Namespace) -> None:
|
||||
|
||||
if args.all:
|
||||
if args.source:
|
||||
if args.source == "owned":
|
||||
names = list_owned_skills()
|
||||
elif args.source == "registry":
|
||||
names = list(load_registry())
|
||||
source = normalize_source(args.source)
|
||||
if source == "builtin":
|
||||
names = list_builtin_skills()
|
||||
elif source == "catalog":
|
||||
names = list(load_catalog())
|
||||
else:
|
||||
_ensure_source_fetched("", args.source)
|
||||
names = list_custom_skills(args.source)[args.source]
|
||||
_ensure_source_fetched("", source)
|
||||
names = list_custom_skills(source)[source]
|
||||
else:
|
||||
names = list_owned_skills()
|
||||
names = list_builtin_skills()
|
||||
targets = resolve_agent_args(["*"])
|
||||
else:
|
||||
names = _collect_skill_names(args.skills, args.skills_flag)
|
||||
@@ -549,9 +562,9 @@ def cmd_select(args: argparse.Namespace) -> None:
|
||||
|
||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||
project_root = None if args.global_scope else _project_root(args.project)
|
||||
registry = load_registry()
|
||||
owned_names = list_owned_skills()
|
||||
for name in [*owned_names, *registry]:
|
||||
catalog = load_catalog()
|
||||
builtin_names = list_builtin_skills()
|
||||
for name in [*builtin_names, *catalog]:
|
||||
validate_skill_name(name)
|
||||
|
||||
def make_choice(
|
||||
@@ -586,69 +599,101 @@ def cmd_select(args: argparse.Namespace) -> None:
|
||||
name=name,
|
||||
installed_name=name,
|
||||
expected=SKILLS_DIR / name,
|
||||
kind="owned",
|
||||
kind="builtin",
|
||||
description=skill_description(name) or "",
|
||||
)
|
||||
for name in owned_names
|
||||
for name in builtin_names
|
||||
]
|
||||
choice_requests: dict[str, tuple[str, str]] = {
|
||||
name: (name, "owned") for name in owned_names
|
||||
name: (name, "builtin") for name in builtin_names
|
||||
}
|
||||
for package, entry in registry.items():
|
||||
skill_names = _registry_skill_names(package, entry)
|
||||
root = external_skill_path(package, entry)
|
||||
if (root / "SKILL.md").is_file():
|
||||
if package in choice_requests:
|
||||
_err(f"警告: registry 条目与 owned skill 同名,已忽略 external: {package}")
|
||||
continue
|
||||
|
||||
def add_provider_choices(
|
||||
provider: str,
|
||||
*,
|
||||
registration: str,
|
||||
discovered: dict[str, Path],
|
||||
description: str,
|
||||
) -> None:
|
||||
source = provider if registration == "custom" else f"catalog:{provider}"
|
||||
if len(discovered) == 1 and provider in discovered:
|
||||
expected = discovered[provider]
|
||||
if provider in choice_requests:
|
||||
_err(
|
||||
f"警告: {registration} source 与已有 skill 同名,"
|
||||
f"已忽略: {provider}"
|
||||
)
|
||||
return
|
||||
choices.append(
|
||||
make_choice(
|
||||
name=package,
|
||||
installed_name=package,
|
||||
expected=root,
|
||||
kind="external",
|
||||
description=str(entry.get("description", "")),
|
||||
name=provider,
|
||||
installed_name=provider,
|
||||
expected=expected,
|
||||
kind=f"{registration}:{provider}",
|
||||
description=description,
|
||||
)
|
||||
)
|
||||
choice_requests[package] = (package, "registry")
|
||||
continue
|
||||
for skill_name in skill_names:
|
||||
expected = root / skill_name
|
||||
choice_requests[provider] = (provider, source)
|
||||
return
|
||||
|
||||
child_names: list[str] = []
|
||||
first_child = len(choices)
|
||||
for skill_name, expected in discovered.items():
|
||||
if (
|
||||
skill_name in owned_names
|
||||
skill_name in builtin_names
|
||||
and expected.resolve() == (SKILLS_DIR / skill_name).resolve()
|
||||
):
|
||||
continue
|
||||
choice_name = f"{package}/{skill_name}"
|
||||
description = read_skill_meta(expected).get("description", "")
|
||||
choice_name = f"{provider}/{skill_name}"
|
||||
choices.append(
|
||||
make_choice(
|
||||
name=choice_name,
|
||||
installed_name=skill_name,
|
||||
expected=expected,
|
||||
kind=f"external:{package}",
|
||||
description=description,
|
||||
kind=f"{registration}:{provider}",
|
||||
description=read_skill_meta(expected).get("description", ""),
|
||||
indent=1,
|
||||
)
|
||||
)
|
||||
choice_requests[choice_name] = (skill_name, f"registry:{package}")
|
||||
child_names = tuple(
|
||||
f"{package}/{skill_name}"
|
||||
for skill_name in skill_names
|
||||
if f"{package}/{skill_name}" in choice_requests
|
||||
)
|
||||
choice_requests[choice_name] = (skill_name, source)
|
||||
child_names.append(choice_name)
|
||||
if child_names:
|
||||
first_child = len(choices) - len(child_names)
|
||||
choices.insert(
|
||||
first_child,
|
||||
SkillChoice(
|
||||
name=package,
|
||||
kind="repository",
|
||||
description=str(entry.get("description", "")),
|
||||
children=child_names,
|
||||
name=provider,
|
||||
kind=f"{registration} source",
|
||||
description=description,
|
||||
children=tuple(child_names),
|
||||
),
|
||||
)
|
||||
|
||||
for package, entry in catalog.items():
|
||||
skill_names = _catalog_skill_names(package, entry)
|
||||
root = catalog_skill_path(package, entry)
|
||||
discovered = (
|
||||
{package: root}
|
||||
if (root / "SKILL.md").is_file()
|
||||
else {skill_name: root / skill_name for skill_name in skill_names}
|
||||
)
|
||||
add_provider_choices(
|
||||
package,
|
||||
registration="catalog",
|
||||
discovered=discovered,
|
||||
description=str(entry.get("description", "")),
|
||||
)
|
||||
|
||||
custom_sources = load_sources()
|
||||
for provider, entry in custom_sources.items():
|
||||
validate_skill_name(provider)
|
||||
_ensure_source_fetched("", provider)
|
||||
add_provider_choices(
|
||||
provider,
|
||||
registration="custom",
|
||||
discovered=discover_source_skills(provider, entry),
|
||||
description=str(entry.get("description", "")),
|
||||
)
|
||||
|
||||
try:
|
||||
scope_label = (
|
||||
"全局"
|
||||
@@ -697,31 +742,13 @@ def cmd_select(args: argparse.Namespace) -> None:
|
||||
entry_targets = targets if args.agents else None
|
||||
for name in sorted(successful):
|
||||
skill_name, source = choice_requests[name]
|
||||
if source == "owned":
|
||||
add_skill_to_manifest(
|
||||
manifest_path,
|
||||
skill_name,
|
||||
source="owned",
|
||||
extra={"targets": entry_targets} if entry_targets else None,
|
||||
)
|
||||
else:
|
||||
package = (
|
||||
skill_name
|
||||
if source == "registry"
|
||||
else source.split(":", 1)[1]
|
||||
)
|
||||
entry = registry[package]
|
||||
extra: dict[str, object] = {"ref": entry.get("ref", "main")}
|
||||
if source != "registry":
|
||||
extra["registry"] = package
|
||||
if entry_targets:
|
||||
extra["targets"] = entry_targets
|
||||
add_skill_to_manifest(
|
||||
manifest_path,
|
||||
skill_name,
|
||||
source="registry",
|
||||
extra=extra,
|
||||
)
|
||||
extra = {"targets": entry_targets} if entry_targets else None
|
||||
add_skill_to_manifest(
|
||||
manifest_path,
|
||||
skill_name,
|
||||
source=source,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
installed_count = len(names) - len(failures)
|
||||
if not names:
|
||||
@@ -745,24 +772,30 @@ def cmd_remove(args: argparse.Namespace) -> None:
|
||||
if not names:
|
||||
raise SystemExit("请指定 skill 名称,或使用 skiff remove --all")
|
||||
|
||||
registry = load_registry()
|
||||
catalog = load_catalog()
|
||||
sources = load_sources()
|
||||
expanded: list[str] = []
|
||||
for spec in names:
|
||||
name, source = split_skill_spec(spec)
|
||||
if source in registry and source not in load_sources():
|
||||
if name not in discover_external_skills(source, registry[source]):
|
||||
if source and source.startswith("catalog:"):
|
||||
provider = source.split(":", 1)[1]
|
||||
if provider not in catalog:
|
||||
raise SystemExit(f"catalog 中不存在: {provider}")
|
||||
if name not in discover_catalog_skills(provider, catalog[provider]):
|
||||
raise SystemExit(
|
||||
f"registry collection {source!r} 中找不到 skill {name!r}"
|
||||
f"catalog source {provider!r} 中找不到 skill {name!r}"
|
||||
)
|
||||
expanded.append(name)
|
||||
elif source is None and name in registry:
|
||||
discovered = discover_external_skills(name, registry[name])
|
||||
root = external_skill_path(name, registry[name])
|
||||
elif source is None and name in catalog:
|
||||
discovered = discover_catalog_skills(name, catalog[name])
|
||||
root = catalog_skill_path(name, catalog[name])
|
||||
expanded.extend(
|
||||
[name]
|
||||
if (root / "SKILL.md").is_file()
|
||||
else list(discovered)
|
||||
)
|
||||
elif source is None and name in sources:
|
||||
expanded.extend(discover_source_skills(name, sources[name]))
|
||||
else:
|
||||
expanded.append(name)
|
||||
|
||||
@@ -787,34 +820,34 @@ def cmd_publish(args: argparse.Namespace) -> None:
|
||||
)
|
||||
|
||||
|
||||
def cmd_registry_add(args: argparse.Namespace) -> None:
|
||||
def cmd_catalog_add(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
validate_skill_name(args.name)
|
||||
registry = load_registry()
|
||||
if args.name in registry:
|
||||
raise SystemExit(f"registry 中已存在: {args.name}")
|
||||
catalog = load_catalog()
|
||||
if args.name in catalog:
|
||||
raise SystemExit(f"catalog 中已存在: {args.name}")
|
||||
|
||||
registry[args.name] = {
|
||||
catalog[args.name] = {
|
||||
"repo": args.repo,
|
||||
"ref": args.ref,
|
||||
"path": args.path,
|
||||
}
|
||||
save_registry(registry)
|
||||
_print(f"已添加 registry 条目: {args.name}")
|
||||
save_catalog(catalog)
|
||||
_print(f"已添加 catalog 条目: {args.name}")
|
||||
|
||||
|
||||
def cmd_fetch(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
registry = load_registry()
|
||||
if args.name not in registry:
|
||||
raise SystemExit(f"registry 中不存在: {args.name}")
|
||||
catalog = load_catalog()
|
||||
if args.name not in catalog:
|
||||
raise SystemExit(f"catalog 中不存在: {args.name}")
|
||||
|
||||
entry = registry[args.name]
|
||||
repo = registry_repo(entry)
|
||||
entry = catalog[args.name]
|
||||
repo = catalog_repo(entry)
|
||||
ref = entry.get("ref", "main")
|
||||
dest = external_checkout_path(args.name, entry)
|
||||
dest = catalog_checkout_path(args.name, entry)
|
||||
|
||||
EXTERNALS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CATALOG_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dest.exists():
|
||||
_print(f"更新: {dest}")
|
||||
@@ -917,40 +950,6 @@ def cmd_source_remove(args: argparse.Namespace) -> None:
|
||||
_print(f"已删除 checkout(不可恢复): {checkout}")
|
||||
|
||||
|
||||
def cmd_enable(args: argparse.Namespace) -> None:
|
||||
_warn_deprecated("skiff enable", "skiff add <name>")
|
||||
ensure_skills_home()
|
||||
validate_skill_name(args.name)
|
||||
root = _project_root(args.project)
|
||||
manifest_path = root / ".skills.yaml"
|
||||
|
||||
name, source = split_skill_spec(args.name)
|
||||
_ensure_source_fetched(name, source)
|
||||
_, resolved_source = resolve_skill_source(name, source=source)
|
||||
add_skill_to_manifest(manifest_path, name, source=resolved_source)
|
||||
|
||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||
_, data = load_manifest(manifest_path)
|
||||
manifest_targets = data.get("targets")
|
||||
if manifest_targets:
|
||||
targets = [t for t in targets if t in manifest_targets]
|
||||
|
||||
_install_skill(name, targets, project_root=root, source=resolved_source)
|
||||
_print(f"已启用项目 skill: {resolved_source}/{name} @ {root}")
|
||||
|
||||
|
||||
def cmd_disable(args: argparse.Namespace) -> None:
|
||||
_warn_deprecated("skiff disable", "skiff remove <name>")
|
||||
root = _project_root(args.project)
|
||||
manifest_path = root / ".skills.yaml"
|
||||
if not remove_skill_from_manifest(manifest_path, args.name):
|
||||
_print(f"manifest 中不存在: {args.name}")
|
||||
return
|
||||
|
||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||
_remove_skill(args.name, targets, project_root=root)
|
||||
|
||||
|
||||
def cmd_sync(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
root = _project_root(args.project)
|
||||
@@ -967,8 +966,6 @@ def cmd_sync(args: argparse.Namespace) -> None:
|
||||
for entry in iter_manifest_skills(data):
|
||||
name = entry["name"]
|
||||
source = entry.get("source")
|
||||
if source == "registry" and entry.get("registry"):
|
||||
source = f"registry:{entry['registry']}"
|
||||
_ensure_source_fetched(name, source)
|
||||
skill_path, _ = resolve_manifest_skill(entry)
|
||||
skill_targets = targets
|
||||
@@ -1022,20 +1019,20 @@ def cmd_create(args: argparse.Namespace) -> None:
|
||||
_print(f"完成后运行: skiff check {args.name} && skiff finalize {args.name}")
|
||||
|
||||
|
||||
def _draft_or_owned_path(name: str) -> tuple[Path, str]:
|
||||
def _draft_or_builtin_path(name: str) -> tuple[Path, str]:
|
||||
draft = DRAFTS_DIR / name
|
||||
if draft.is_dir():
|
||||
return draft, "草稿"
|
||||
owned = SKILLS_DIR / name
|
||||
if owned.is_dir():
|
||||
return owned, "正式 skill"
|
||||
builtin = SKILLS_DIR / name
|
||||
if builtin.is_dir():
|
||||
return builtin, "正式 skill"
|
||||
raise SystemExit(f"找不到草稿或正式 skill: {name}")
|
||||
|
||||
|
||||
def cmd_check(args: argparse.Namespace) -> None:
|
||||
ensure_skills_home()
|
||||
validate_skill_name(args.name)
|
||||
path, kind = _draft_or_owned_path(args.name)
|
||||
path, kind = _draft_or_builtin_path(args.name)
|
||||
issues = validate_skill_dir(path, args.name)
|
||||
if kind == "草稿":
|
||||
issues = [issue for issue in issues if "草稿文件: brief.yaml" not in issue]
|
||||
@@ -1090,8 +1087,8 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
||||
_err("✗ ~/.skills 未正确配置")
|
||||
issues += 1
|
||||
|
||||
for name in list_owned_skills():
|
||||
for target, link, expected in _installed_links(name, targets, source="owned"):
|
||||
for name in list_builtin_skills():
|
||||
for target, link, expected in _installed_links(name, targets, source="builtin"):
|
||||
status = check_link(link, expected)
|
||||
if status.ok:
|
||||
continue
|
||||
@@ -1104,9 +1101,9 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_err(f" 修复失败: {exc}")
|
||||
|
||||
registry = load_registry()
|
||||
for name in registry:
|
||||
ext = external_skill_path(name, registry[name])
|
||||
catalog = load_catalog()
|
||||
for name in catalog:
|
||||
ext = catalog_skill_path(name, catalog[name])
|
||||
if not ext.exists():
|
||||
_err(f"✗ 外部 skill 未 fetch: {name}")
|
||||
issues += 1
|
||||
@@ -1224,7 +1221,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
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.add_argument("--source", help="只列出指定来源(builtin、catalog 或 custom source)")
|
||||
p_list.set_defaults(func=cmd_list)
|
||||
|
||||
p_status = sub.add_parser("status", help="安装状态总览")
|
||||
@@ -1233,13 +1230,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
p_add = sub.add_parser(
|
||||
"add",
|
||||
help="安装 skill 到 agent(自研或 registry)",
|
||||
description="安装 ~/.skills 中的自研 skill,或 registry 中的外部 skill",
|
||||
help="安装 builtin、catalog 或 custom source 中的 skill",
|
||||
description="安装 builtin、catalog 或 custom source 中的 skill",
|
||||
)
|
||||
p_add.add_argument("skills", nargs="*", metavar="skill", help="skill 名称(可多个)")
|
||||
p_add.add_argument("-s", "--skill", dest="skills_flag", action="append", metavar="SKILL")
|
||||
p_add.add_argument("--list", dest="list_available", action="store_true", help="列出可用自研 skill,不安装")
|
||||
p_add.add_argument("--all", action="store_true", help="安装全部自研 skill 到全部 agent")
|
||||
p_add.add_argument("--list", dest="list_available", action="store_true", help="列出可用 builtin skill,不安装")
|
||||
p_add.add_argument("--all", action="store_true", help="安装指定来源的全部 skill 到全部 agent")
|
||||
p_add.add_argument("--source", help="指定 skill 来源(也可使用 source/name)")
|
||||
_add_common_flags(p_add)
|
||||
p_add.set_defaults(func=cmd_add)
|
||||
@@ -1272,20 +1269,20 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
p_publish.add_argument("--no-commit", action="store_true", help="只 git add,不 commit")
|
||||
p_publish.set_defaults(func=cmd_publish)
|
||||
|
||||
p_registry = sub.add_parser("registry", help="管理 registry.yaml 中的外部 skill")
|
||||
registry_sub = p_registry.add_subparsers(dest="registry_command", required=True)
|
||||
p_reg_add = registry_sub.add_parser("add", help="注册外部 Git skill")
|
||||
p_reg_add.add_argument("name", help="registry 名称")
|
||||
p_reg_add.add_argument("repo", help="Git 仓库 URL")
|
||||
p_reg_add.add_argument("--ref", default="main", help="分支或 tag(默认 main)")
|
||||
p_reg_add.add_argument("--path", default=".", help="仓库内子路径(默认 .)")
|
||||
p_reg_add.set_defaults(func=cmd_registry_add)
|
||||
p_catalog = sub.add_parser("catalog", help="管理 catalog.yaml 中的预置来源")
|
||||
catalog_sub = p_catalog.add_subparsers(dest="catalog_command", required=True)
|
||||
p_catalog_add = catalog_sub.add_parser("add", help="添加预置 Git source")
|
||||
p_catalog_add.add_argument("name", help="catalog source 名称")
|
||||
p_catalog_add.add_argument("repo", help="Git 仓库 URL")
|
||||
p_catalog_add.add_argument("--ref", default="main", help="分支或 tag(默认 main)")
|
||||
p_catalog_add.add_argument("--path", default=".", help="仓库内子路径(默认 .)")
|
||||
p_catalog_add.set_defaults(func=cmd_catalog_add)
|
||||
|
||||
p_fetch = sub.add_parser("fetch", help="拉取/更新 registry 中的外部 skill")
|
||||
p_fetch.add_argument("name", help="registry 名称")
|
||||
p_fetch = sub.add_parser("fetch", help="拉取/更新 catalog source")
|
||||
p_fetch.add_argument("name", help="catalog 名称")
|
||||
p_fetch.set_defaults(func=cmd_fetch)
|
||||
|
||||
p_source = sub.add_parser("source", help="管理包含多个 skills 的自定义仓库")
|
||||
p_source = sub.add_parser("source", help="管理自定义 Skill source")
|
||||
source_sub = p_source.add_subparsers(dest="source_command", required=True)
|
||||
|
||||
p_source_add = source_sub.add_parser("add", help="注册 Git 或本地 skill source")
|
||||
@@ -1319,18 +1316,6 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
p_source_remove.set_defaults(func=cmd_source_remove)
|
||||
|
||||
p_enable = sub.add_parser("enable", help=argparse.SUPPRESS)
|
||||
p_enable.add_argument("name")
|
||||
p_enable.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||
p_enable.add_argument("--project")
|
||||
p_enable.set_defaults(func=cmd_enable)
|
||||
|
||||
p_disable = sub.add_parser("disable", help=argparse.SUPPRESS)
|
||||
p_disable.add_argument("name")
|
||||
p_disable.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||
p_disable.add_argument("--project")
|
||||
p_disable.set_defaults(func=cmd_disable)
|
||||
|
||||
p_sync = sub.add_parser("sync", help="按 .skills.yaml 重建项目软链")
|
||||
p_sync.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||
p_sync.add_argument("--project")
|
||||
|
||||
Reference in New Issue
Block a user