diff --git a/docs/source/en/guides/cli.md b/docs/source/en/guides/cli.md index 0522055e69..0db5aebf7e 100644 --- a/docs/source/en/guides/cli.md +++ b/docs/source/en/guides/cli.md @@ -114,6 +114,18 @@ You can also install the CLI using [Homebrew](https://brew.sh/): Check out the Homebrew huggingface page [here](https://formulae.brew.sh/formula/hf) for more details. +### Updating + +To upgrade to the latest version, run: + +```bash +>>> hf update +``` + +This detects how `hf` was installed (Homebrew, standalone installer, or pip) and runs the matching update command. + +By default, the CLI also prints a one-line yellow warning to stderr when a newer version is available on PyPI. To silence it (e.g. in offline CI), set `HF_HUB_DISABLE_UPDATE_CHECK=1`. + ## hf auth login In many cases, you must be logged in to a Hugging Face account to interact with the Hub (download private repos, upload files, create PRs, etc.). To do so, you need a [User Access Token](https://huggingface.co/docs/hub/security-tokens) from your [Settings page](https://huggingface.co/settings/tokens). The User Access Token is used to authenticate your identity to the Hub. Make sure to set a token with write access if you want to upload or modify content. diff --git a/docs/source/en/installation.md b/docs/source/en/installation.md index 380c62aa5a..0bc77723e5 100644 --- a/docs/source/en/installation.md +++ b/docs/source/en/installation.md @@ -118,6 +118,8 @@ On Windows: powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex" ``` +To upgrade an existing install, run `hf update` — it detects how `hf` was installed (standalone installer, Homebrew, or pip) and runs the matching command. + ## Install with conda If you are more familiar with it, you can install `huggingface_hub` using the [conda-forge channel](https://anaconda.org/conda-forge/huggingface_hub): diff --git a/docs/source/en/package_reference/cli.md b/docs/source/en/package_reference/cli.md index 5c6388cbfd..84d1dcac8c 100644 --- a/docs/source/en/package_reference/cli.md +++ b/docs/source/en/package_reference/cli.md @@ -42,6 +42,7 @@ $ hf [OPTIONS] COMMAND [ARGS]... * `skills`: Manage skills for AI assistants. * `spaces`: Interact with spaces on the Hub. * `sync`: Sync files between local directory and a... +* `update`: Update the `hf` CLI to the latest version. * `upload`: Upload a file or a folder to the Hub. * `upload-large-folder`: Upload a large folder to the Hub. * `version`: Print information about the hf version. @@ -3765,6 +3766,20 @@ $ hf sync [OPTIONS] [SOURCE] [DEST] * `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens. * `--help`: Show this message and exit. +## `hf update` + +Update the `hf` CLI to the latest version. + +**Usage**: + +```console +$ hf update [OPTIONS] +``` + +**Options**: + +* `--help`: Show this message and exit. + ## `hf upload` Upload a file or a folder to the Hub. Recommended for single-commit uploads. diff --git a/docs/source/en/package_reference/environment_variables.md b/docs/source/en/package_reference/environment_variables.md index 851ddc74a1..418f7516da 100644 --- a/docs/source/en/package_reference/environment_variables.md +++ b/docs/source/en/package_reference/environment_variables.md @@ -181,6 +181,12 @@ Each library defines its own policy (i.e. which usage to monitor) but the core i You can set `HF_HUB_DISABLE_TELEMETRY=1` as environment variable to globally disable telemetry. +### HF_HUB_DISABLE_UPDATE_CHECK + +By default, the `hf` CLI checks PyPI for a newer release at startup (at most once every 24 hours) and prints a one-line yellow warning to stderr when one is available, suggesting `hf update`. The check is already a no-op on dev and pre-release versions. + +Set `HF_HUB_DISABLE_UPDATE_CHECK=1` to skip the PyPI request and silence the warning entirely. Useful in offline CI environments or when you prefer a quieter shell output. + ### HF_HUB_DISABLE_XET Set to disable using `hf-xet`, even if it is available in your Python environment. This is since `hf-xet` will be used automatically if it is found, this allows explicitly disabling its usage. If you are disabling Xet, please consider [filing an issue and including the diagnostics](https://github.com/huggingface/xet-core?tab=readme-ov-file#issues-diagnostics--debugging) information to help us understand why Xet is not working for you. diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index e6b7944a80..4b2bb54aa9 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -35,7 +35,6 @@ from huggingface_hub import Volume, __version__, constants from huggingface_hub.errors import CLIError from huggingface_hub.utils import ( - ANSI, disable_progress_bars, get_session, hf_raise_for_status, @@ -953,10 +952,10 @@ def check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> Non """ Check whether a newer version of a library is available on PyPI. - If a newer version is found and stdin/stderr are attached to a TTY, prompt the user to update interactively. - Otherwise (non-TTY or update command cannot be determined), print a warning to stderr. + If a newer version is found, print a hint pointing at `hf update`. If current version is a pre-release (e.g. `1.0.0.rc1`), or a dev version (e.g. `1.0.0.dev1`), no check is performed. + If `HF_HUB_DISABLE_UPDATE_CHECK` is set, the check is skipped entirely. This function is called at the entry point of the CLI. It only performs the check once every 24 hours, and any error during the check is caught and logged, to avoid breaking the CLI. @@ -972,6 +971,9 @@ def check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> Non def _check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> None: + if constants.HF_HUB_DISABLE_UPDATE_CHECK: + return + current_version = importlib.metadata.version(library) # Skip if current version is a pre-release or dev version @@ -1002,81 +1004,28 @@ def _check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> No else: update_command = _get_transformers_update_command() - if sys.stdin.isatty() and sys.stderr.isatty() and update_command is not None: - _prompt_autoupdate(library, current_version, latest_version, update_command) - else: - display_cmd = " ".join(update_command) if update_command else None - update_hint = f"To update, run: {ANSI.bold(display_cmd)}" if display_cmd else "" - click.echo( - ANSI.yellow( - f"A new version of {library} ({latest_version}) is available! " - f"You are using version {current_version}." + (f"\n{update_hint}" if update_hint else "") + "\n" - ), - file=sys.stderr, - ) + message = f"A new version of {library} ({latest_version}) is available! You are using version {current_version}." + if update_command is not None: + match library: + case "huggingface_hub": + message += "\nTo update, run: hf update" + case _: + message += f"\nTo update, run: {' '.join(update_command)}" + out.hint(message) -def _prompt_autoupdate( - library: str, - current_version: str, - latest_version: str, - update_command: list[str], -) -> None: - """Interactively ask the user if they want to update, and run the update command if accepted. +def run_update() -> int: + """Run the install-method-appropriate update command for the `hf` CLI. - After a successful update the CLI exits so the user can re-run their command with the new version. - All output goes to stderr to keep stdout clean for command output. + Raises CLIError if the installation method can't be determined. + Returns the subprocess exit code on success/failure of the update itself. """ - display_cmd = " ".join(update_command) - - click.echo("", file=sys.stderr) - click.echo( - ANSI.yellow(f" A new version of {library} is available: {current_version} → {latest_version}"), - file=sys.stderr, - ) - click.echo("", file=sys.stderr) - - click.echo( - ANSI.yellow(" Do you want to update now? [Y/n] ") + ANSI.gray(f"({display_cmd})") + " ", - file=sys.stderr, - nl=False, - ) - try: - raw_answer = sys.stdin.readline() - except (EOFError, KeyboardInterrupt): - click.echo("", file=sys.stderr) - return - - if raw_answer == "": - # EOF (e.g. Ctrl+D) — treat as cancellation, not acceptance - click.echo("", file=sys.stderr) - return - - answer = raw_answer.strip().lower() # Note: if user press 'Enter', raw_answer is `\n` - if answer in ("", "y", "yes"): - click.echo("", file=sys.stderr) - click.echo(ANSI.gray(f" Running: {display_cmd}"), file=sys.stderr) - click.echo("", file=sys.stderr) - returncode = subprocess.call(update_command) - if returncode == 0: - click.echo("", file=sys.stderr) - click.echo( - ANSI.green(f" ✓ Successfully updated {library} to {latest_version}. Please re-run your command."), - file=sys.stderr, - ) - raise SystemExit(0) - else: - click.echo("", file=sys.stderr) - click.echo( - ANSI.red(f" ✗ Update failed (exit code {returncode}). Please update manually."), - file=sys.stderr, - ) - else: - click.echo( - ANSI.gray(f" Skipped. You can update later with: {display_cmd}"), - file=sys.stderr, + cmd = _get_huggingface_hub_update_command() + if cmd is None: + raise CLIError( + "Cannot determine how to update huggingface_hub (unknown installation method). Please update manually." ) - click.echo("", file=sys.stderr) + return subprocess.call(cmd) def _get_huggingface_hub_update_command() -> list[str] | None: diff --git a/src/huggingface_hub/cli/hf.py b/src/huggingface_hub/cli/hf.py index b4eb37502c..cde7a41e55 100644 --- a/src/huggingface_hub/cli/hf.py +++ b/src/huggingface_hub/cli/hf.py @@ -42,7 +42,7 @@ from huggingface_hub.cli.repos import repos_cli from huggingface_hub.cli.skills import skills_cli from huggingface_hub.cli.spaces import spaces_cli -from huggingface_hub.cli.system import env, version +from huggingface_hub.cli.system import env, update, version from huggingface_hub.cli.upload import UPLOAD_EXAMPLES, upload from huggingface_hub.cli.upload_large_folder import UPLOAD_LARGE_FOLDER_EXAMPLES, upload_large_folder from huggingface_hub.cli.webhooks import webhooks_cli @@ -80,6 +80,7 @@ def app_callback( app.command(examples=UPLOAD_LARGE_FOLDER_EXAMPLES)(upload_large_folder) app.command(topic="help")(env) +app.command(topic="help")(update) app.command(topic="help")(version) app.command(hidden=True)(lfs_enable_largefiles) diff --git a/src/huggingface_hub/cli/skills.py b/src/huggingface_hub/cli/skills.py index 2b09d467e1..2a8d26cc9d 100644 --- a/src/huggingface_hub/cli/skills.py +++ b/src/huggingface_hub/cli/skills.py @@ -101,6 +101,7 @@ - Use `hf --help` for full options, descriptions, usage, and real-world examples - Authenticate with `HF_TOKEN` env var (recommended) or with `--token` +- Update the CLI with `hf update` (uses the correct command for the detected install method) """ CENTRAL_LOCAL = Path(".agents/skills") diff --git a/src/huggingface_hub/cli/system.py b/src/huggingface_hub/cli/system.py index ede66925bb..a1b7ff3b6f 100644 --- a/src/huggingface_hub/cli/system.py +++ b/src/huggingface_hub/cli/system.py @@ -13,9 +13,13 @@ # limitations under the License. """Contains commands to print information about the environment and version.""" +import typer + from huggingface_hub import __version__ from ..utils import dump_environment_info +from ._cli_utils import run_update +from ._output import out def env() -> None: @@ -26,3 +30,14 @@ def env() -> None: def version() -> None: """Print information about the hf version.""" print(__version__) + + +def update() -> None: + """Update the `hf` CLI to the latest version.""" + returncode = run_update() + if returncode != 0: + raise typer.Exit(code=returncode) + out.hint( + "You may also want to run `hf skills upgrade` to refresh any installed skills " + "so your AI agent sees the latest command surface." + ) diff --git a/src/huggingface_hub/constants.py b/src/huggingface_hub/constants.py index 568843c17e..f851d3f748 100644 --- a/src/huggingface_hub/constants.py +++ b/src/huggingface_hub/constants.py @@ -194,6 +194,9 @@ def list_files(repo_id: str): # Check is performed once per 24 hours at most. CHECK_FOR_UPDATE_DONE_PATH = os.path.join(HF_HOME, ".check_for_update_done") +# Set to skip the CLI update check (PyPI query + "new version available" warning at startup). +HF_HUB_DISABLE_UPDATE_CHECK = _is_true(os.environ.get("HF_HUB_DISABLE_UPDATE_CHECK")) + # If set, log level will be set to DEBUG and all requests made to the Hub will be logged # as curl commands for reproducibility. HF_DEBUG = _is_true(os.environ.get("HF_DEBUG"))