Skip to content

Rename CLI to 'hf' + reorganize syntax #3229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 23, 2025
Merged
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def get_version() -> str:
entry_points={
"console_scripts": [
"huggingface-cli=huggingface_hub.commands.huggingface_cli:main",
"hf=huggingface_hub.cli.hf:main",
"tiny-agents=huggingface_hub.inference._mcp.cli:app",
],
"fsspec.specs": "hf=huggingface_hub.HfFileSystem",
Expand Down
27 changes: 27 additions & 0 deletions src/huggingface_hub/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 202 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.

from abc import ABC, abstractmethod
from argparse import _SubParsersAction


class BaseHuggingfaceCLICommand(ABC):
@staticmethod
@abstractmethod
def register_subcommand(parser: _SubParsersAction):
raise NotImplementedError()

@abstractmethod
def run(self):
raise NotImplementedError()
69 changes: 69 additions & 0 deletions src/huggingface_hub/cli/_cli_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2022 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 a utility for good-looking prints."""

import os
from typing import List, Union


class ANSI:
"""
Helper for en.wikipedia.org/wiki/ANSI_escape_code
"""

_bold = "\u001b[1m"
_gray = "\u001b[90m"
_red = "\u001b[31m"
_reset = "\u001b[0m"
_yellow = "\u001b[33m"

@classmethod
def bold(cls, s: str) -> str:
return cls._format(s, cls._bold)

@classmethod
def gray(cls, s: str) -> str:
return cls._format(s, cls._gray)

@classmethod
def red(cls, s: str) -> str:
return cls._format(s, cls._bold + cls._red)

@classmethod
def yellow(cls, s: str) -> str:
return cls._format(s, cls._yellow)

@classmethod
def _format(cls, s: str, code: str) -> str:
if os.environ.get("NO_COLOR"):
# See https://no-color.org/
return s
return f"{code}{s}{cls._reset}"


def tabulate(rows: List[List[Union[str, int]]], headers: List[str]) -> str:
"""
Inspired by:

- stackoverflow.com/a/8356620/593036
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data
"""
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)]
row_format = ("{{:{}}} " * len(headers)).format(*col_widths)
lines = []
lines.append(row_format.format(*headers))
lines.append(row_format.format(*["-" * w for w in col_widths]))
for row in rows:
lines.append(row_format.format(*row))
return "\n".join(lines)
210 changes: 210 additions & 0 deletions src/huggingface_hub/cli/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Copyright 2020 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 authenticate to the Hugging Face Hub and interact with your repositories.

Usage:
# login and save token locally.
hf auth login --token=hf_*** --add-to-git-credential

# switch between tokens
hf auth switch

# list all tokens
hf auth list

# logout from all tokens
hf auth logout

# check which account you are logged in as
hf auth whoami
"""

from argparse import _SubParsersAction
from typing import List, Optional

from requests.exceptions import HTTPError

from huggingface_hub.commands import BaseHuggingfaceCLICommand
from huggingface_hub.constants import ENDPOINT
from huggingface_hub.hf_api import HfApi

from .._login import auth_list, auth_switch, login, logout
from ..utils import get_stored_tokens, get_token, logging
from ._cli_utils import ANSI


logger = logging.get_logger(__name__)

try:
from InquirerPy import inquirer
from InquirerPy.base.control import Choice

_inquirer_py_available = True
except ImportError:
_inquirer_py_available = False


class AuthCommands(BaseHuggingfaceCLICommand):
@staticmethod
def register_subcommand(parser: _SubParsersAction):
# Create the main 'auth' command
auth_parser = parser.add_parser("auth", help="Manage authentication (login, logout, etc.).")
auth_subparsers = auth_parser.add_subparsers(help="Authentication subcommands")

# Add 'login' as a subcommand of 'auth'
login_parser = auth_subparsers.add_parser(
"login", help="Log in using a token from huggingface.co/settings/tokens"
)
login_parser.add_argument(
"--token",
type=str,
help="Token generated from https://huggingface.co/settings/tokens",
)
login_parser.add_argument(
"--add-to-git-credential",
action="store_true",
help="Optional: Save token to git credential helper.",
)
login_parser.set_defaults(func=lambda args: AuthLogin(args))

# Add 'logout' as a subcommand of 'auth'
logout_parser = auth_subparsers.add_parser("logout", help="Log out")
logout_parser.add_argument(
"--token-name",
type=str,
help="Optional: Name of the access token to log out from.",
)
logout_parser.set_defaults(func=lambda args: AuthLogout(args))

# Add 'whoami' as a subcommand of 'auth'
whoami_parser = auth_subparsers.add_parser(
"whoami", help="Find out which huggingface.co account you are logged in as."
)
whoami_parser.set_defaults(func=lambda args: AuthWhoami(args))

# Existing subcommands
auth_switch_parser = auth_subparsers.add_parser("switch", help="Switch between access tokens")
auth_switch_parser.add_argument(
"--token-name",
type=str,
help="Optional: Name of the access token to switch to.",
)
auth_switch_parser.add_argument(
"--add-to-git-credential",
action="store_true",
help="Optional: Save token to git credential helper.",
)
auth_switch_parser.set_defaults(func=lambda args: AuthSwitch(args))

auth_list_parser = auth_subparsers.add_parser("list", help="List all stored access tokens")
auth_list_parser.set_defaults(func=lambda args: AuthList(args))


class BaseAuthCommand:
def __init__(self, args):
self.args = args
self._api = HfApi()


class AuthLogin(BaseAuthCommand):
def run(self):
logging.set_verbosity_info()
login(
token=self.args.token,
add_to_git_credential=self.args.add_to_git_credential,
)


class AuthLogout(BaseAuthCommand):
def run(self):
logging.set_verbosity_info()
logout(token_name=self.args.token_name)


class AuthSwitch(BaseAuthCommand):
def run(self):
logging.set_verbosity_info()
token_name = self.args.token_name
if token_name is None:
token_name = self._select_token_name()

if token_name is None:
print("No token name provided. Aborting.")
exit()
auth_switch(token_name, add_to_git_credential=self.args.add_to_git_credential)

def _select_token_name(self) -> Optional[str]:
token_names = list(get_stored_tokens().keys())

if not token_names:
logger.error("No stored tokens found. Please login first.")
return None

if _inquirer_py_available:
return self._select_token_name_tui(token_names)
# if inquirer is not available, use a simpler terminal UI
print("Available stored tokens:")
for i, token_name in enumerate(token_names, 1):
print(f"{i}. {token_name}")
while True:
try:
choice = input("Enter the number of the token to switch to (or 'q' to quit): ")
if choice.lower() == "q":
return None
index = int(choice) - 1
if 0 <= index < len(token_names):
return token_names[index]
else:
print("Invalid selection. Please try again.")
except ValueError:
print("Invalid input. Please enter a number or 'q' to quit.")

def _select_token_name_tui(self, token_names: List[str]) -> Optional[str]:
choices = [Choice(token_name, name=token_name) for token_name in token_names]
try:
return inquirer.select(
message="Select a token to switch to:",
choices=choices,
default=None,
).execute()
except KeyboardInterrupt:
logger.info("Token selection cancelled.")
return None


class AuthList(BaseAuthCommand):
def run(self):
logging.set_verbosity_info()
auth_list()


class AuthWhoami(BaseAuthCommand):
def run(self):
token = get_token()
if token is None:
print("Not logged in")
exit()
try:
info = self._api.whoami(token)
print(info["name"])
orgs = [org["name"] for org in info["orgs"]]
if orgs:
print(ANSI.bold("orgs: "), ",".join(orgs))

if ENDPOINT != "https://huggingface.co":
print(f"Authenticated through private endpoint: {ENDPOINT}")
except HTTPError as e:
print(e)
print(ANSI.red(e.response.text))
exit(1)
Loading