You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
6.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Copyright 2025 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains commands to manage skills for AI assistants.
Usage:
# install the hf-cli skill for Claude (project-level, in current directory)
hf skills add --claude
# install for multiple assistants (project-level)
hf skills add --claude --codex --opencode
# install globally (user-level)
hf skills add --claude --global
# install to a custom directory
hf skills add --dest=~/my-skills
# overwrite an existing skill
hf skills add --claude --force
"""
import os
import shutil
from pathlib import Path
from typing import Annotated, Optional
import typer
from huggingface_hub.errors import CLIError
from huggingface_hub.utils import get_session
from ._cli_utils import typer_factory
DEFAULT_SKILL_ID = "hf-cli"
_GITHUB_RAW_BASE = "https://raw.githubusercontent.com/huggingface/huggingface_hub/main/docs/source/en"
_SKILL_MD_URL = f"{_GITHUB_RAW_BASE}/guides/cli.md"
_REFERENCE_URL = f"{_GITHUB_RAW_BASE}/package_reference/cli.md"
_SKILL_YAML_PREFIX = """\
---
name: hf-cli
description: >
Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing
repositories, models, datasets, and Spaces on the Hugging Face Hub.
---
The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface_cli` command.
Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
"""
CENTRAL_LOCAL = Path(".agents/skills")
CENTRAL_GLOBAL = Path("~/.agents/skills")
GLOBAL_TARGETS = {
"codex": Path("~/.codex/skills"),
"claude": Path("~/.claude/skills"),
"opencode": Path("~/.config/opencode/skills"),
}
LOCAL_TARGETS = {
"codex": Path(".codex/skills"),
"claude": Path(".claude/skills"),
"opencode": Path(".opencode/skills"),
}
skills_cli = typer_factory(help="Manage skills for AI assistants.")
def _download(url: str) -> str:
"""Download text content from a URL."""
response = get_session().get(url)
response.raise_for_status()
return response.text
def _remove_existing(path: Path, force: bool) -> None:
"""Remove existing file/directory/symlink if force is True, otherwise raise an error."""
if not (path.exists() or path.is_symlink()):
return
if not force:
raise SystemExit(f"Skill already exists at {path}.\nRe-run with --force to overwrite.")
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def _install_to(skills_dir: Path, force: bool) -> Path:
"""Download and install the skill files into a skills directory. Returns the installed path."""
skills_dir = skills_dir.expanduser().resolve()
skills_dir.mkdir(parents=True, exist_ok=True)
dest = skills_dir / DEFAULT_SKILL_ID
_remove_existing(dest, force)
dest.mkdir()
# SKILL.md the main guide, prefixed with YAML metadata
skill_content = _download(_SKILL_MD_URL)
(dest / "SKILL.md").write_text(_SKILL_YAML_PREFIX + skill_content, encoding="utf-8")
# references/cli.md the full CLI reference
ref_dir = dest / "references"
ref_dir.mkdir()
ref_content = _download(_REFERENCE_URL)
(ref_dir / "cli.md").write_text(ref_content, encoding="utf-8")
return dest
def _create_symlink(agent_skills_dir: Path, central_skill_path: Path, force: bool) -> Path:
"""Create a relative symlink from agent directory to the central skill location."""
agent_skills_dir = agent_skills_dir.expanduser().resolve()
agent_skills_dir.mkdir(parents=True, exist_ok=True)
link_path = agent_skills_dir / DEFAULT_SKILL_ID
_remove_existing(link_path, force)
link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir))
return link_path
@skills_cli.command(
"add",
examples=[
"hf skills add --claude",
"hf skills add --claude --global",
"hf skills add --codex --opencode",
],
)
def skills_add(
claude: Annotated[bool, typer.Option("--claude", help="Install for Claude.")] = False,
codex: Annotated[bool, typer.Option("--codex", help="Install for Codex.")] = False,
opencode: Annotated[bool, typer.Option("--opencode", help="Install for OpenCode.")] = False,
global_: Annotated[
bool,
typer.Option(
"--global",
"-g",
help="Install globally (user-level) instead of in the current project directory.",
),
] = False,
dest: Annotated[
Optional[Path],
typer.Option(
help="Install into a custom destination (path to skills directory).",
),
] = None,
force: Annotated[
bool,
typer.Option(
"--force",
help="Overwrite existing skills in the destination.",
),
] = False,
) -> None:
"""Download a skill and install it for an AI assistant."""
if not (claude or codex or opencode or dest):
raise CLIError("Pick a destination via --claude, --codex, --opencode, or --dest.")
if dest:
if claude or codex or opencode or global_:
print("--dest cannot be combined with --claude, --codex, --opencode, or --global.")
raise typer.Exit(code=1)
skill_dest = _install_to(dest, force)
print(f"Installed '{DEFAULT_SKILL_ID}' to {skill_dest}")
return
targets_dict = GLOBAL_TARGETS if global_ else LOCAL_TARGETS
agent_targets: list[Path] = []
if claude:
agent_targets.append(targets_dict["claude"])
if codex:
agent_targets.append(targets_dict["codex"])
if opencode:
agent_targets.append(targets_dict["opencode"])
central_path = CENTRAL_GLOBAL if global_ else CENTRAL_LOCAL
central_skill_path = _install_to(central_path, force)
print(f"Installed '{DEFAULT_SKILL_ID}' to central location: {central_skill_path}")
for agent_target in agent_targets:
link_path = _create_symlink(agent_target, central_skill_path, force)
print(f"Created symlink: {link_path}")