refactor: unify skill source model

This commit is contained in:
2026-07-30 12:11:25 +08:00
parent 076d87e303
commit 7d1994cf93
18 changed files with 669 additions and 527 deletions
+238 -253
View File
@@ -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")