feat(skiff): align CLI with Vercel skills and fix entry symlink
Replace install/uninstall with add/remove, add publish for git ops in ~/.skills, and resolve bin/skiff symlinks so commands work from any cwd. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
|
# 跟随软链,确保从 ~/.local/bin/skiff 调用时仍能找到仓库根目录
|
||||||
|
SCRIPT="${BASH_SOURCE[0]}"
|
||||||
|
while [ -L "$SCRIPT" ]; do
|
||||||
|
link_dir="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||||
|
SCRIPT="$(readlink "$SCRIPT")"
|
||||||
|
[[ "$SCRIPT" != /* ]] && SCRIPT="$link_dir/$SCRIPT"
|
||||||
|
done
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
|
||||||
|
|
||||||
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:$PYTHONPATH}"
|
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:$PYTHONPATH}"
|
||||||
exec python3 -m skiff "$@"
|
exec python3 -m skiff "$@"
|
||||||
|
|||||||
+35
-9
@@ -9,7 +9,29 @@ cd /path/to/skills # 本仓库根目录
|
|||||||
./install.sh # 软链到 ~/.local/bin/skiff
|
./install.sh # 软链到 ~/.local/bin/skiff
|
||||||
```
|
```
|
||||||
|
|
||||||
确保 `~/.local/bin` 在 `PATH` 中。开发时也可直接运行:
|
确保 `~/.local/bin` 在 `PATH` 中。
|
||||||
|
|
||||||
|
## 命令风格
|
||||||
|
|
||||||
|
接口对齐 [Vercel skills CLI](https://github.com/vercel-labs/skills) 的 `add` / `remove`,专用于 **~/.skills 自研 skill**。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 浏览可用自研 skill
|
||||||
|
skiff add --list
|
||||||
|
|
||||||
|
# 装到当前项目 / 全局
|
||||||
|
skiff add discussion-notes -a cursor -y
|
||||||
|
skiff add discussion-notes -a cursor -g -y
|
||||||
|
|
||||||
|
# 卸载
|
||||||
|
skiff remove discussion-notes -a cursor -y
|
||||||
|
skiff rm discussion-notes -g -y
|
||||||
|
|
||||||
|
# 改完 skill 后提交推送(在任意目录执行,操作 ~/.skills)
|
||||||
|
skiff publish skills/discussion-notes -m "update discussion-notes" --push
|
||||||
|
```
|
||||||
|
|
||||||
|
开发时也可直接运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PYTHONPATH=/path/to/skills python3 -m skiff <command>
|
PYTHONPATH=/path/to/skills python3 -m skiff <command>
|
||||||
@@ -42,8 +64,12 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `skiff install <name> [--target all]` | 软链到 Agent 全局目录 |
|
| `skiff add <name> [--global] [-a AGENT...] [-y]` | 安装到 Agent 目录(软链) |
|
||||||
| `skiff uninstall <name> [--target all]` | 移除软链 |
|
| `skiff remove <name> [--global] [-a AGENT...] [-y]` | 移除软链(`rm` / `r` 别名) |
|
||||||
|
| `skiff add --list` | 列出可用自研 skill |
|
||||||
|
| `skiff publish [paths] -m MSG [--push]` | 在 ~/.skills 内 git add/commit/push |
|
||||||
|
|
||||||
|
旧命令 `install` / `uninstall` 已移除,请改用 `add` / `remove`。
|
||||||
|
|
||||||
全局目标路径:
|
全局目标路径:
|
||||||
|
|
||||||
@@ -57,9 +83,9 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `skiff add <name> <repo-url> [--ref main] [--path .]` | 写入 `registry.yaml` |
|
| `skiff registry add <name> <repo-url> [--ref main] [--path .]` | 写入 `registry.yaml` |
|
||||||
| `skiff fetch <name>` | 克隆/更新到 `~/.local/share/skills/externals/<name>/` |
|
| `skiff fetch <name>` | 克隆/更新到 `~/.local/share/skills/externals/<name>/` |
|
||||||
| `skiff install-external <name> [--target all]` | 安装外部 skill 到 Agent 目录 |
|
| `skiff add <name> [-g] [-a AGENT...]` | 安装 registry 中的外部 skill(缺失时自动 fetch) |
|
||||||
|
|
||||||
### 项目级
|
### 项目级
|
||||||
|
|
||||||
@@ -91,16 +117,16 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
```bash
|
```bash
|
||||||
skiff create my-skill
|
skiff create my-skill
|
||||||
# 编辑 skills/my-skill/SKILL.md
|
# 编辑 skills/my-skill/SKILL.md
|
||||||
skiff install my-skill --target cursor
|
skiff publish skills/my-skill -m "add my-skill" --push
|
||||||
skiff doctor --target cursor
|
skiff add my-skill -a cursor -g -y
|
||||||
|
skiff doctor -a cursor
|
||||||
```
|
```
|
||||||
|
|
||||||
### 在项目中启用 skill
|
### 在项目中启用 skill
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/code/my-app
|
cd ~/code/my-app
|
||||||
skiff enable declarative-openspec-loop --target cursor
|
skiff add declarative-openspec-loop -a cursor -y
|
||||||
skiff sync
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 安装外部 Git skill
|
### 安装外部 Git skill
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
"""skiff — Agent Skills 安装与管理 CLI。"""
|
"""skiff — Agent Skills 安装与管理 CLI。"""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.2.0"
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Agent 名称解析(兼容 Vercel skills CLI 别名)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from skiff.paths import ALL_TARGETS
|
||||||
|
|
||||||
|
AGENT_ALIASES: dict[str, str] = {
|
||||||
|
"cursor": "cursor",
|
||||||
|
"claude": "claude",
|
||||||
|
"claude-code": "claude",
|
||||||
|
"codex": "codex",
|
||||||
|
"*": "*",
|
||||||
|
}
|
||||||
|
|
||||||
|
AGENT_CHOICES = sorted({*ALL_TARGETS, *AGENT_ALIASES.keys()})
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_agent_args(groups: list[list[str]] | None) -> list[str] | None:
|
||||||
|
if not groups:
|
||||||
|
return None
|
||||||
|
flat = [item for group in groups for item in group]
|
||||||
|
return flat or None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_agent_args(agents: list[str] | None) -> list[str]:
|
||||||
|
if not agents or agents == ["*"] or "*" in agents:
|
||||||
|
return list(ALL_TARGETS)
|
||||||
|
|
||||||
|
resolved: list[str] = []
|
||||||
|
for raw in agents:
|
||||||
|
key = raw.lower()
|
||||||
|
if key == "*":
|
||||||
|
return list(ALL_TARGETS)
|
||||||
|
target = AGENT_ALIASES.get(key)
|
||||||
|
if target is None:
|
||||||
|
choices = ", ".join(AGENT_CHOICES)
|
||||||
|
raise SystemExit(f"未知 agent: {raw!r},可选: {choices}")
|
||||||
|
if target != "*" and target not in resolved:
|
||||||
|
resolved.append(target)
|
||||||
|
return resolved
|
||||||
+255
-105
@@ -7,19 +7,18 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from skiff import __version__
|
from skiff import __version__
|
||||||
|
from skiff.agents import flatten_agent_args, resolve_agent_args
|
||||||
|
from skiff.gitops import publish as git_publish
|
||||||
from skiff.paths import (
|
from skiff.paths import (
|
||||||
ALL_TARGETS,
|
ALL_TARGETS,
|
||||||
EXTERNALS_DIR,
|
EXTERNALS_DIR,
|
||||||
REGISTRY_FILE,
|
|
||||||
SKILLS_DIR,
|
SKILLS_DIR,
|
||||||
SKILLS_HOME,
|
SKILLS_HOME,
|
||||||
TEMPLATE_DIR,
|
TEMPLATE_DIR,
|
||||||
agent_skill_dir,
|
agent_skill_dir,
|
||||||
ensure_skills_home,
|
ensure_skills_home,
|
||||||
resolve_targets,
|
|
||||||
)
|
)
|
||||||
from skiff.project import (
|
from skiff.project import (
|
||||||
add_skill_to_manifest,
|
add_skill_to_manifest,
|
||||||
@@ -30,7 +29,13 @@ from skiff.project import (
|
|||||||
save_manifest,
|
save_manifest,
|
||||||
)
|
)
|
||||||
from skiff.registry import external_skill_path, load_registry, save_registry
|
from skiff.registry import external_skill_path, load_registry, save_registry
|
||||||
from skiff.skills import list_owned_skills, owned_skill_path, resolve_skill_source, validate_skill_name
|
from skiff.skills import (
|
||||||
|
list_owned_skills,
|
||||||
|
owned_skill_path,
|
||||||
|
resolve_skill_source,
|
||||||
|
skill_description,
|
||||||
|
validate_skill_name,
|
||||||
|
)
|
||||||
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
||||||
|
|
||||||
|
|
||||||
@@ -42,12 +47,94 @@ def _err(msg: str) -> None:
|
|||||||
print(msg, file=sys.stderr)
|
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()
|
||||||
|
return find_repo_root() or Path.cwd()
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_skill_names(positional: list[str] | None, flagged: list[str] | None) -> list[str]:
|
||||||
|
names = list(positional or [])
|
||||||
|
if flagged:
|
||||||
|
names.extend(flagged)
|
||||||
|
if "*" in names:
|
||||||
|
return list_owned_skills()
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_external_fetched(name: str) -> None:
|
||||||
|
registry = load_registry()
|
||||||
|
if name not in registry:
|
||||||
|
return
|
||||||
|
|
||||||
|
entry = registry[name]
|
||||||
|
path = external_skill_path(name, entry)
|
||||||
|
if path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
repo = entry["repo"]
|
||||||
|
ref = entry.get("ref", "main")
|
||||||
|
dest = EXTERNALS_DIR / name
|
||||||
|
EXTERNALS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
_print(f"拉取外部 skill: {name}")
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", "--branch", ref, "--", repo, str(dest)],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_skill(name: str, targets: list[str], project_root: Path | None = None) -> None:
|
||||||
|
_ensure_external_fetched(name)
|
||||||
|
skill_path, _ = resolve_skill_source(name)
|
||||||
|
for target in targets:
|
||||||
|
link = agent_skill_dir(target, project_root=project_root) / name
|
||||||
|
create_link(link, skill_path)
|
||||||
|
scope = "全局" if project_root is None else "项目"
|
||||||
|
_print(f"已安装 ({scope}/{target}): {name} -> {skill_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _list_installed_names(project_root: Path | None, targets: list[str]) -> list[str]:
|
||||||
|
names: set[str] = set()
|
||||||
|
for target in targets:
|
||||||
|
skill_dir = agent_skill_dir(target, project_root=project_root)
|
||||||
|
if not skill_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for entry in skill_dir.iterdir():
|
||||||
|
if entry.name.startswith("."):
|
||||||
|
continue
|
||||||
|
if entry.is_symlink() or entry.is_dir():
|
||||||
|
names.add(entry.name)
|
||||||
|
return sorted(names)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_skill(
|
||||||
|
name: str,
|
||||||
|
targets: list[str],
|
||||||
|
*,
|
||||||
|
project_root: Path | None,
|
||||||
|
) -> int:
|
||||||
|
removed = 0
|
||||||
|
for target in targets:
|
||||||
|
link = agent_skill_dir(target, project_root=project_root) / name
|
||||||
|
try:
|
||||||
|
if remove_link(link):
|
||||||
|
scope = "全局" if project_root is None else "项目"
|
||||||
|
_print(f"已移除 ({scope}/{target}): {name}")
|
||||||
|
removed += 1
|
||||||
|
except FileExistsError as exc:
|
||||||
|
_err(f"跳过 {link}: {exc}")
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
def cmd_setup(args: argparse.Namespace) -> None:
|
def cmd_setup(args: argparse.Namespace) -> None:
|
||||||
repo = Path(args.path).resolve()
|
repo = Path(args.path).resolve()
|
||||||
if not (repo / "skills").is_dir():
|
if not (repo / "skills").is_dir():
|
||||||
raise SystemExit(f"不是有效的 skills 仓库(缺少 skills/): {repo}")
|
raise SystemExit(f"不是有效的 skills 仓库(缺少 skills/): {repo}")
|
||||||
|
|
||||||
# 开发场景:仓库本身就在 ~/.skills
|
|
||||||
if repo == SKILLS_HOME.resolve() and SKILLS_HOME.is_dir() and not SKILLS_HOME.is_symlink():
|
if repo == SKILLS_HOME.resolve() and SKILLS_HOME.is_dir() and not SKILLS_HOME.is_symlink():
|
||||||
_print(f"skills 仓库已在 ~/.skills: {repo}")
|
_print(f"skills 仓库已在 ~/.skills: {repo}")
|
||||||
return
|
return
|
||||||
@@ -95,13 +182,13 @@ def _installed_links(name: str, targets: list[str], project_root: Path | None =
|
|||||||
|
|
||||||
def cmd_status(args: argparse.Namespace) -> None:
|
def cmd_status(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
targets = resolve_targets(args.target)
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
owned = list_owned_skills()
|
owned = list_owned_skills()
|
||||||
registry = load_registry()
|
registry = load_registry()
|
||||||
all_names = owned + [n for n in registry if n not in owned]
|
all_names = owned + [n for n in registry if n not in owned]
|
||||||
|
|
||||||
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
||||||
_print(f"targets: {', '.join(targets)}\n")
|
_print(f"agents: {', '.join(targets)}\n")
|
||||||
|
|
||||||
for name in all_names:
|
for name in all_names:
|
||||||
kind = "owned" if name in owned else "external"
|
kind = "owned" if name in owned else "external"
|
||||||
@@ -119,33 +206,81 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|||||||
_print("")
|
_print("")
|
||||||
|
|
||||||
|
|
||||||
def _install_skill(name: str, targets: list[str], project_root: Path | None = None) -> None:
|
def _print_available_skills() -> None:
|
||||||
skill_path, kind = resolve_skill_source(name)
|
|
||||||
for target in targets:
|
|
||||||
link = agent_skill_dir(target, project_root=project_root) / name
|
|
||||||
create_link(link, skill_path)
|
|
||||||
scope = "项目" if project_root else "全局"
|
|
||||||
_print(f"已安装 ({scope}/{target}): {name} -> {skill_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_install(args: argparse.Namespace) -> None:
|
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
validate_skill_name(args.name)
|
owned = list_owned_skills()
|
||||||
targets = resolve_targets(args.target)
|
if not owned:
|
||||||
_install_skill(args.name, targets)
|
_print("~/.skills/skills/ 中没有自研 skill")
|
||||||
|
return
|
||||||
|
|
||||||
|
_print(f"来源: {SKILLS_DIR}\n")
|
||||||
def cmd_uninstall(args: argparse.Namespace) -> None:
|
for name in owned:
|
||||||
ensure_skills_home()
|
desc = skill_description(name)
|
||||||
targets = resolve_targets(args.target)
|
_print(f" {name}")
|
||||||
for target in targets:
|
if desc:
|
||||||
link = agent_skill_dir(target) / args.name
|
for line in desc.splitlines():
|
||||||
if remove_link(link):
|
_print(f" {line}")
|
||||||
_print(f"已移除 ({target}): {link}")
|
_print("")
|
||||||
|
|
||||||
|
|
||||||
def cmd_add(args: argparse.Namespace) -> None:
|
def cmd_add(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
|
|
||||||
|
if args.list_available:
|
||||||
|
_print_available_skills()
|
||||||
|
_print("使用 skiff add <name> 安装,或 skiff add <name> -g 全局安装")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.all:
|
||||||
|
names = list_owned_skills()
|
||||||
|
targets = resolve_agent_args(["*"])
|
||||||
|
else:
|
||||||
|
names = _collect_skill_names(args.skills, args.skills_flag)
|
||||||
|
if not names:
|
||||||
|
raise SystemExit("请指定 skill 名称,或使用 skiff add --list 查看可用 skill")
|
||||||
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
|
|
||||||
|
project_root = None if args.global_scope else _project_root(args.project)
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
validate_skill_name(name)
|
||||||
|
_install_skill(name, targets, project_root=project_root)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(args: argparse.Namespace) -> None:
|
||||||
|
ensure_skills_home()
|
||||||
|
project_root = None if args.global_scope else _project_root(args.project)
|
||||||
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
|
|
||||||
|
if args.all:
|
||||||
|
names = _list_installed_names(project_root, targets)
|
||||||
|
else:
|
||||||
|
names = _collect_skill_names(args.skills, args.skills_flag)
|
||||||
|
|
||||||
|
if not names:
|
||||||
|
raise SystemExit("请指定 skill 名称,或使用 skiff remove --all")
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for name in names:
|
||||||
|
total += _remove_skill(name, targets, project_root=project_root)
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
_print("没有移除任何 skill")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_publish(args: argparse.Namespace) -> None:
|
||||||
|
ensure_skills_home()
|
||||||
|
paths = args.paths or ["."]
|
||||||
|
git_publish(
|
||||||
|
paths=paths,
|
||||||
|
message=args.message,
|
||||||
|
push=args.push,
|
||||||
|
no_commit=args.no_commit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_registry_add(args: argparse.Namespace) -> None:
|
||||||
|
ensure_skills_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
registry = load_registry()
|
registry = load_registry()
|
||||||
if args.name in registry:
|
if args.name in registry:
|
||||||
@@ -186,48 +321,22 @@ def cmd_fetch(args: argparse.Namespace) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def cmd_install_external(args: argparse.Namespace) -> None:
|
|
||||||
ensure_skills_home()
|
|
||||||
registry = load_registry()
|
|
||||||
if args.name not in registry:
|
|
||||||
raise SystemExit(f"registry 中不存在: {args.name}")
|
|
||||||
|
|
||||||
path = external_skill_path(args.name, registry[args.name])
|
|
||||||
if not path.exists():
|
|
||||||
raise SystemExit(f"请先 fetch: skiff fetch {args.name}")
|
|
||||||
|
|
||||||
targets = resolve_targets(args.target)
|
|
||||||
for target in targets:
|
|
||||||
link = agent_skill_dir(target) / args.name
|
|
||||||
create_link(link, path)
|
|
||||||
_print(f"已安装外部 ({target}): {args.name} -> {path}")
|
|
||||||
|
|
||||||
|
|
||||||
def _project_root(args: argparse.Namespace) -> Path:
|
|
||||||
if args.project:
|
|
||||||
root = Path(args.project).resolve()
|
|
||||||
else:
|
|
||||||
root = find_repo_root() or Path.cwd()
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_enable(args: argparse.Namespace) -> None:
|
def cmd_enable(args: argparse.Namespace) -> None:
|
||||||
|
_warn_deprecated("skiff enable", "skiff add <name>")
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
root = _project_root(args)
|
root = _project_root(args.project)
|
||||||
manifest_path = root / ".skills.yaml"
|
manifest_path = root / ".skills.yaml"
|
||||||
|
|
||||||
# 判断来源
|
|
||||||
registry = load_registry()
|
registry = load_registry()
|
||||||
if args.name in registry:
|
if args.name in registry:
|
||||||
source = "registry"
|
|
||||||
extra = {"source": "registry", "ref": registry[args.name].get("ref", "main")}
|
extra = {"source": "registry", "ref": registry[args.name].get("ref", "main")}
|
||||||
add_skill_to_manifest(manifest_path, args.name, source="registry", extra=extra)
|
add_skill_to_manifest(manifest_path, args.name, source="registry", extra=extra)
|
||||||
else:
|
else:
|
||||||
owned_skill_path(args.name) # 验证存在
|
owned_skill_path(args.name)
|
||||||
add_skill_to_manifest(manifest_path, args.name, source="owned")
|
add_skill_to_manifest(manifest_path, args.name, source="owned")
|
||||||
|
|
||||||
targets = resolve_targets(args.target)
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
_, data = load_manifest(manifest_path)
|
_, data = load_manifest(manifest_path)
|
||||||
manifest_targets = data.get("targets")
|
manifest_targets = data.get("targets")
|
||||||
if manifest_targets:
|
if manifest_targets:
|
||||||
@@ -238,28 +347,26 @@ def cmd_enable(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_disable(args: argparse.Namespace) -> None:
|
def cmd_disable(args: argparse.Namespace) -> None:
|
||||||
root = _project_root(args)
|
_warn_deprecated("skiff disable", "skiff remove <name>")
|
||||||
|
root = _project_root(args.project)
|
||||||
manifest_path = root / ".skills.yaml"
|
manifest_path = root / ".skills.yaml"
|
||||||
if not remove_skill_from_manifest(manifest_path, args.name):
|
if not remove_skill_from_manifest(manifest_path, args.name):
|
||||||
_print(f"manifest 中不存在: {args.name}")
|
_print(f"manifest 中不存在: {args.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
targets = resolve_targets(args.target)
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
for target in targets:
|
_remove_skill(args.name, targets, project_root=root)
|
||||||
link = agent_skill_dir(target, project_root=root) / args.name
|
|
||||||
if remove_link(link):
|
|
||||||
_print(f"已禁用 ({target}): {link}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_sync(args: argparse.Namespace) -> None:
|
def cmd_sync(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
root = _project_root(args)
|
root = _project_root(args.project)
|
||||||
manifest_path = root / ".skills.yaml"
|
manifest_path = root / ".skills.yaml"
|
||||||
if not manifest_path.is_file():
|
if not manifest_path.is_file():
|
||||||
raise SystemExit(f"未找到 {manifest_path}")
|
raise SystemExit(f"未找到 {manifest_path}")
|
||||||
|
|
||||||
_, data = load_manifest(manifest_path)
|
_, data = load_manifest(manifest_path)
|
||||||
targets = resolve_targets(args.target)
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
manifest_targets = data.get("targets")
|
manifest_targets = data.get("targets")
|
||||||
if manifest_targets:
|
if manifest_targets:
|
||||||
targets = [t for t in targets if t in manifest_targets]
|
targets = [t for t in targets if t in manifest_targets]
|
||||||
@@ -293,12 +400,14 @@ def cmd_create(args: argparse.Namespace) -> None:
|
|||||||
)
|
)
|
||||||
skill_md.write_text(content, encoding="utf-8")
|
skill_md.write_text(content, encoding="utf-8")
|
||||||
_print(f"已创建 skill: {dst}")
|
_print(f"已创建 skill: {dst}")
|
||||||
_print(f"下一步: 编辑 {skill_md},然后 skiff install {args.name}")
|
_print(f"下一步: 编辑 {skill_md}")
|
||||||
|
_print(f" skiff publish skills/{args.name} -m \"add {args.name}\" --push")
|
||||||
|
_print(f" skiff add {args.name} -a cursor -g -y")
|
||||||
|
|
||||||
|
|
||||||
def cmd_doctor(args: argparse.Namespace) -> None:
|
def cmd_doctor(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
targets = resolve_targets(args.target)
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
issues = 0
|
issues = 0
|
||||||
|
|
||||||
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
||||||
@@ -344,67 +453,108 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_common_flags(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--agent",
|
||||||
|
dest="agents",
|
||||||
|
nargs="+",
|
||||||
|
action="append",
|
||||||
|
metavar="AGENT",
|
||||||
|
help="目标 agent(cursor、claude、claude-code、codex、*)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-g",
|
||||||
|
"--global",
|
||||||
|
dest="global_scope",
|
||||||
|
action="store_true",
|
||||||
|
help="全局安装/卸载(默认当前项目)",
|
||||||
|
)
|
||||||
|
parser.add_argument("-y", "--yes", action="store_true", help="跳过确认(兼容 Vercel skills)")
|
||||||
|
parser.add_argument("--project", help="项目根目录(默认自动检测或当前目录)")
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="skiff",
|
prog="skiff",
|
||||||
description="Agent Skills 安装与管理 CLI",
|
description="自研 Agent Skills 安装与管理 CLI(接口对齐 Vercel skills)",
|
||||||
)
|
)
|
||||||
parser.add_argument("--version", action="version", version=f"skiff {__version__}")
|
parser.add_argument("--version", action="version", version=f"skiff {__version__}")
|
||||||
|
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
p_setup = sub.add_parser("setup", help="关联 ~/.skills 到本仓库")
|
p_setup = sub.add_parser("setup", help="关联 ~/.skills 到 skills 仓库")
|
||||||
p_setup.add_argument("path", help="skills 仓库路径")
|
p_setup.add_argument("path", nargs="?", default=str(SKILLS_HOME), help="仓库路径(默认 ~/.skills)")
|
||||||
p_setup.set_defaults(func=cmd_setup)
|
p_setup.set_defaults(func=cmd_setup)
|
||||||
|
|
||||||
p_list = sub.add_parser("list", help="列出所有 skill")
|
p_list = sub.add_parser("list", help="列出 ~/.skills 中的 skill 目录")
|
||||||
p_list.set_defaults(func=cmd_list)
|
p_list.set_defaults(func=cmd_list)
|
||||||
|
|
||||||
p_status = sub.add_parser("status", help="安装状态总览")
|
p_status = sub.add_parser("status", help="安装状态总览")
|
||||||
p_status.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_status.add_argument("-a", "--agent", dest="agents", nargs="+", action="append", metavar="AGENT")
|
||||||
p_status.set_defaults(func=cmd_status)
|
p_status.set_defaults(func=cmd_status)
|
||||||
|
|
||||||
p_install = sub.add_parser("install", help="全局安装 skill(软链)")
|
p_add = sub.add_parser(
|
||||||
p_install.add_argument("name", help="skill 名称")
|
"add",
|
||||||
p_install.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
help="安装 skill 到 agent(自研或 registry)",
|
||||||
p_install.set_defaults(func=cmd_install)
|
description="安装 ~/.skills 中的自研 skill,或 registry 中的外部 skill",
|
||||||
|
)
|
||||||
p_uninstall = sub.add_parser("uninstall", help="移除全局软链")
|
p_add.add_argument("skills", nargs="*", metavar="skill", help="skill 名称(可多个)")
|
||||||
p_uninstall.add_argument("name", help="skill 名称")
|
p_add.add_argument("-s", "--skill", dest="skills_flag", action="append", metavar="SKILL")
|
||||||
p_uninstall.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_add.add_argument("--list", dest="list_available", action="store_true", help="列出可用自研 skill,不安装")
|
||||||
p_uninstall.set_defaults(func=cmd_uninstall)
|
p_add.add_argument("--all", action="store_true", help="安装全部自研 skill 到全部 agent")
|
||||||
|
_add_common_flags(p_add)
|
||||||
p_add = sub.add_parser("add", help="添加外部 skill 到 registry")
|
|
||||||
p_add.add_argument("name", help="registry 名称")
|
|
||||||
p_add.add_argument("repo", help="Git 仓库 URL")
|
|
||||||
p_add.add_argument("--ref", default="main", help="分支或 tag(默认 main)")
|
|
||||||
p_add.add_argument("--path", default=".", help="仓库内子路径(默认 .)")
|
|
||||||
p_add.set_defaults(func=cmd_add)
|
p_add.set_defaults(func=cmd_add)
|
||||||
|
|
||||||
p_fetch = sub.add_parser("fetch", help="拉取外部 Git skill")
|
p_remove = sub.add_parser(
|
||||||
|
"remove",
|
||||||
|
aliases=["rm", "r"],
|
||||||
|
help="从 agent 移除 skill",
|
||||||
|
)
|
||||||
|
p_remove.add_argument("skills", nargs="*", metavar="skill", help="skill 名称(可多个)")
|
||||||
|
p_remove.add_argument("-s", "--skill", dest="skills_flag", action="append", metavar="SKILL")
|
||||||
|
p_remove.add_argument("--all", action="store_true", help="移除当前范围内全部 skill")
|
||||||
|
_add_common_flags(p_remove)
|
||||||
|
p_remove.set_defaults(func=cmd_remove)
|
||||||
|
|
||||||
|
p_publish = sub.add_parser(
|
||||||
|
"publish",
|
||||||
|
help="在 ~/.skills 内 git add / commit / push",
|
||||||
|
)
|
||||||
|
p_publish.add_argument("paths", nargs="*", help="要提交的路径(默认 .)")
|
||||||
|
p_publish.add_argument("-m", "--message", help="commit 说明")
|
||||||
|
p_publish.add_argument("--push", action="store_true", help="commit 后 push")
|
||||||
|
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_fetch = sub.add_parser("fetch", help="拉取/更新 registry 中的外部 skill")
|
||||||
p_fetch.add_argument("name", help="registry 名称")
|
p_fetch.add_argument("name", help="registry 名称")
|
||||||
p_fetch.set_defaults(func=cmd_fetch)
|
p_fetch.set_defaults(func=cmd_fetch)
|
||||||
|
|
||||||
p_inst_ext = sub.add_parser("install-external", help="安装外部 skill 到 Agent 目录")
|
p_enable = sub.add_parser("enable", help=argparse.SUPPRESS)
|
||||||
p_inst_ext.add_argument("name", help="registry 名称")
|
p_enable.add_argument("name")
|
||||||
p_inst_ext.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_enable.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_inst_ext.set_defaults(func=cmd_install_external)
|
p_enable.add_argument("--project")
|
||||||
|
|
||||||
p_enable = sub.add_parser("enable", help="项目级启用 skill")
|
|
||||||
p_enable.add_argument("name", help="skill 名称")
|
|
||||||
p_enable.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
|
||||||
p_enable.add_argument("--project", help="项目根目录(默认自动检测)")
|
|
||||||
p_enable.set_defaults(func=cmd_enable)
|
p_enable.set_defaults(func=cmd_enable)
|
||||||
|
|
||||||
p_disable = sub.add_parser("disable", help="项目级禁用 skill")
|
p_disable = sub.add_parser("disable", help=argparse.SUPPRESS)
|
||||||
p_disable.add_argument("name", help="skill 名称")
|
p_disable.add_argument("name")
|
||||||
p_disable.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_disable.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_disable.add_argument("--project", help="项目根目录(默认自动检测)")
|
p_disable.add_argument("--project")
|
||||||
p_disable.set_defaults(func=cmd_disable)
|
p_disable.set_defaults(func=cmd_disable)
|
||||||
|
|
||||||
p_sync = sub.add_parser("sync", help="按 .skills.yaml 重建项目软链")
|
p_sync = sub.add_parser("sync", help="按 .skills.yaml 重建项目软链")
|
||||||
p_sync.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_sync.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_sync.add_argument("--project", help="项目根目录(默认自动检测)")
|
p_sync.add_argument("--project")
|
||||||
p_sync.set_defaults(func=cmd_sync)
|
p_sync.set_defaults(func=cmd_sync)
|
||||||
|
|
||||||
p_create = sub.add_parser("create", help="从 _template 创建自研 skill")
|
p_create = sub.add_parser("create", help="从 _template 创建自研 skill")
|
||||||
@@ -412,7 +562,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
p_create.set_defaults(func=cmd_create)
|
p_create.set_defaults(func=cmd_create)
|
||||||
|
|
||||||
p_doctor = sub.add_parser("doctor", help="软链健康检查")
|
p_doctor = sub.add_parser("doctor", help="软链健康检查")
|
||||||
p_doctor.add_argument("--target", choices=[*ALL_TARGETS, "all"], default="all")
|
p_doctor.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")
|
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")
|
||||||
p_doctor.set_defaults(func=cmd_doctor)
|
p_doctor.set_defaults(func=cmd_doctor)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""~/.skills 仓库内的 git 操作。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from skiff.paths import SKILLS_HOME
|
||||||
|
|
||||||
|
|
||||||
|
def _run_git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(
|
||||||
|
["git", "-C", str(repo), *args],
|
||||||
|
check=check,
|
||||||
|
text=True,
|
||||||
|
capture_output=not sys.stdout.isatty(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_git_repo(repo: Path) -> None:
|
||||||
|
if not (repo / ".git").exists():
|
||||||
|
raise SystemExit(f"不是 git 仓库: {repo}")
|
||||||
|
|
||||||
|
|
||||||
|
def has_staged_changes(repo: Path) -> bool:
|
||||||
|
result = _run_git(repo, "diff", "--cached", "--quiet", check=False)
|
||||||
|
return result.returncode == 1
|
||||||
|
|
||||||
|
|
||||||
|
def publish(
|
||||||
|
*,
|
||||||
|
paths: list[str],
|
||||||
|
message: str | None,
|
||||||
|
push: bool,
|
||||||
|
no_commit: bool,
|
||||||
|
) -> None:
|
||||||
|
repo = SKILLS_HOME.resolve()
|
||||||
|
ensure_git_repo(repo)
|
||||||
|
|
||||||
|
_run_git(repo, "add", "--", *paths)
|
||||||
|
|
||||||
|
if not has_staged_changes(repo):
|
||||||
|
print("没有可提交的变更", file=sys.stdout)
|
||||||
|
return
|
||||||
|
|
||||||
|
if no_commit:
|
||||||
|
print(f"已暂存变更: {', '.join(paths)}", file=sys.stdout)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
raise SystemExit("提交需要 -m/--message")
|
||||||
|
|
||||||
|
_run_git(repo, "commit", "-m", message)
|
||||||
|
print(f"已提交: {message}", file=sys.stdout)
|
||||||
|
|
||||||
|
if push:
|
||||||
|
_run_git(repo, "push")
|
||||||
|
print("已推送到远程", file=sys.stdout)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
|
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
|
||||||
@@ -53,6 +54,45 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
|
|||||||
raise SystemExit(f"找不到 skill: {name}")
|
raise SystemExit(f"找不到 skill: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_skill_meta(skill_dir: Path) -> dict[str, str]:
|
||||||
|
skill_md = skill_dir / "SKILL.md"
|
||||||
|
if not skill_md.is_file():
|
||||||
|
return {}
|
||||||
|
text = skill_md.read_text(encoding="utf-8")
|
||||||
|
if not text.startswith("---"):
|
||||||
|
return {}
|
||||||
|
end = text.find("\n---", 3)
|
||||||
|
if end == -1:
|
||||||
|
return {}
|
||||||
|
frontmatter = text[3:end]
|
||||||
|
meta: dict[str, str] = {}
|
||||||
|
|
||||||
|
name_match = re.search(r"^name:\s*(.+)$", frontmatter, re.MULTILINE)
|
||||||
|
if name_match:
|
||||||
|
meta["name"] = name_match.group(1).strip().strip("\"'")
|
||||||
|
|
||||||
|
desc_match = re.search(
|
||||||
|
r"^description:\s*(?:>-|>\||>|-)?\s*\n((?:[ \t].+\n?)+)",
|
||||||
|
frontmatter,
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
if desc_match:
|
||||||
|
lines = [line.strip() for line in desc_match.group(1).splitlines() if line.strip()]
|
||||||
|
meta["description"] = " ".join(lines)
|
||||||
|
else:
|
||||||
|
inline = re.search(r"^description:\s*(.+)$", frontmatter, re.MULTILINE)
|
||||||
|
if inline:
|
||||||
|
meta["description"] = inline.group(1).strip().strip("\"'")
|
||||||
|
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def skill_description(name: str) -> str | None:
|
||||||
|
meta = read_skill_meta(SKILLS_DIR / name)
|
||||||
|
desc = meta.get("description")
|
||||||
|
return desc.strip() if desc else None
|
||||||
|
|
||||||
|
|
||||||
def validate_skill_name(name: str) -> None:
|
def validate_skill_name(name: str) -> None:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: skiff
|
||||||
|
description: >-
|
||||||
|
维护 ~/.skills 自研 skill:编辑后用 skiff publish 提交推送,用 skiff add/remove 在各项目或全局挂卸 skill。
|
||||||
|
触发词:skiff、自研 skill、publish skill、安装自研 skill、更新 skill 到项目。
|
||||||
|
---
|
||||||
|
|
||||||
|
# skiff 自研 Skill 工作流
|
||||||
|
|
||||||
|
SSOT 固定在 `~/.skills/skills/<name>/`。内容通过 **symlink** 分发到各 agent,改 SSOT 即全项目生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 日常流程
|
||||||
|
|
||||||
|
### 1. 改 skill
|
||||||
|
|
||||||
|
直接编辑:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.skills/skills/<name>/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 提交并推送(不用离开当前目录)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff publish skills/<name> -m "update <name>" --push
|
||||||
|
```
|
||||||
|
|
||||||
|
只暂存不提交:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff publish skills/<name> --no-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 装到项目或全局
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 当前项目
|
||||||
|
cd ~/code/my-app
|
||||||
|
skiff add discussion-notes -a cursor -y
|
||||||
|
|
||||||
|
# 全局(所有项目)
|
||||||
|
skiff add discussion-notes -a cursor -g -y
|
||||||
|
|
||||||
|
# 多个 agent
|
||||||
|
skiff add discussion-notes -a cursor -a codex -g -y
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 卸载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff remove discussion-notes -a cursor -y # 当前项目
|
||||||
|
skiff remove discussion-notes -g -a cursor -y # 全局
|
||||||
|
skiff rm discussion-notes -g -y # rm 别名
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 浏览可用自研 skill
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff add --list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与 Vercel `npx skills` 的分工
|
||||||
|
|
||||||
|
| 场景 | 工具 |
|
||||||
|
|------|------|
|
||||||
|
| 自研 skill(~/.skills) | **skiff** |
|
||||||
|
| 社区 skill(GitHub 任意仓库) | `npx skills add` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命令对照
|
||||||
|
|
||||||
|
| skiff | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `add <name> [-g] [-a AGENT...] [-y]` | 安装 |
|
||||||
|
| `remove <name> [-g] [-a AGENT...] [-y]` | 卸载(`rm` / `r` 别名) |
|
||||||
|
| `add --list` | 列出可用自研 skill |
|
||||||
|
| `publish [paths] -m MSG [--push]` | git add / commit / push |
|
||||||
|
| `list` | 列出 ~/.skills 目录结构 |
|
||||||
|
| `status` | 查看软链安装状态 |
|
||||||
|
| `create <name>` | 新建自研 skill |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 不要在 `project/.agents/skills/` 里直接改文件;应改 `~/.skills/skills/` 再 `publish`
|
||||||
|
- symlink 正确时,**不需要 reinstall**;保存 SSOT 后各项目自动读到新内容
|
||||||
|
- 社区 skill 用 `npx skills add`,不要用 skiff `registry add` 除非团队要 pin 版本
|
||||||
Reference in New Issue
Block a user