From 9cb6ec12336b307eb47264dd7937fa017c4683bb Mon Sep 17 00:00:00 2001 From: ajspig Date: Wed, 26 Aug 2026 11:32:31 -0400 Subject: [PATCH] feat(cli): check for newer version --- docs/v3/documentation/reference/cli.mdx | 3 + honcho-cli/CHANGELOG.md | 5 +- honcho-cli/README.md | 1 + honcho-cli/src/honcho_cli/_help.py | 2 + honcho-cli/src/honcho_cli/main.py | 2 + honcho-cli/src/honcho_cli/update_check.py | 67 +++++++++++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 honcho-cli/src/honcho_cli/update_check.py diff --git a/docs/v3/documentation/reference/cli.mdx b/docs/v3/documentation/reference/cli.mdx index 5df031d6..b442c690 100644 --- a/docs/v3/documentation/reference/cli.mdx +++ b/docs/v3/documentation/reference/cli.mdx @@ -59,6 +59,7 @@ The CLI resolves config in this order: **flag → env var → config file → de | Peer | — | `HONCHO_PEER_ID` | `-p` / `--peer` | No | | Session | — | `HONCHO_SESSION_ID` | `-s` / `--session` | No | | JSON output | — | `HONCHO_JSON` | `--json` | No | +| Update nag | — | `HONCHO_NO_UPDATE_CHECK` | — | No | ### Persisted config @@ -110,6 +111,8 @@ Every command adapts its output to the context: - **Piped or redirected** — JSON automatically (detected via `isatty`). - **`--json` flag / `HONCHO_JSON=1`** — force JSON regardless of terminal. +Interactive sessions may print a one-line upgrade hint on stderr at most once a day when a newer `honcho-cli` is on PyPI (`uv tool upgrade honcho-cli`). JSON/piped output skips it; set `HONCHO_NO_UPDATE_CHECK=1` to disable it. + Collection commands emit JSON arrays; single-resource commands emit JSON objects. Errors are always structured: ```json diff --git a/honcho-cli/CHANGELOG.md b/honcho-cli/CHANGELOG.md index d48c739f..80b7e5ef 100644 --- a/honcho-cli/CHANGELOG.md +++ b/honcho-cli/CHANGELOG.md @@ -9,13 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.1.4] - 2026-08-26 -### Changed +### Added -- `--setup` API key prompts echo `*` per character so a paste is visibly received instead of a blank getpass field +- A TTY notice when a newer `honcho-cli` is on PyPI (`uv tool upgrade honcho-cli`). Skipped in JSON mode; disable with `HONCHO_NO_UPDATE_CHECK` ### Fixed - `--setup` for openai-compatible writes `EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL` into the profile `.env` alongside `LLM_OPENAI_BASE_URL` (#1068) +- `--setup` API key prompts echo `*` per character so a paste is visibly received instead of a blank getpass field ## [0.1.3] - 2026-08-25 diff --git a/honcho-cli/README.md b/honcho-cli/README.md index f1585a89..2d8e86ae 100644 --- a/honcho-cli/README.md +++ b/honcho-cli/README.md @@ -186,6 +186,7 @@ Precedence (highest first): **flag → env var → config file → default**. | `HONCHO_PEER_ID` | `-p` / `--peer` | Peer scope | | `HONCHO_SESSION_ID` | `-s` / `--session` | Session scope | | `HONCHO_JSON` | `--json` | Force JSON output (`1` / `true`) | +| `HONCHO_NO_UPDATE_CHECK` | — | Disable the once-a-day upgrade notice (`1` / `true`) | | `HONCHO_PROFILE` | `--profile` (start/stop/status) | Local stack profile (default: `local`) | | `LLM_OPENAI_API_KEY` | — | Provider key for `honcho start` (also `LLM_ANTHROPIC_API_KEY`, `LLM_GEMINI_API_KEY`) | diff --git a/honcho-cli/src/honcho_cli/_help.py b/honcho-cli/src/honcho_cli/_help.py index 936e26b7..dbb4f7cd 100644 --- a/honcho-cli/src/honcho_cli/_help.py +++ b/honcho-cli/src/honcho_cli/_help.py @@ -24,6 +24,7 @@ from typer.core import TyperGroup from honcho_cli import __version__ from honcho_cli.branding import BANNER, BRAND from honcho_cli.output import use_json +from honcho_cli.update_check import maybe_print_update_nag # Theme Typer's rich help renderer. Module-level side effect limited to @@ -113,6 +114,7 @@ def print_welcome(console: Console) -> None: console.print(_welcome_panel("memory", memory_rows)) console.print(_welcome_panel("options", option_rows)) console.print() + maybe_print_update_nag() class HonchoTyperGroup(TyperGroup): diff --git a/honcho-cli/src/honcho_cli/main.py b/honcho-cli/src/honcho_cli/main.py index 8a9c16f6..d2282c81 100644 --- a/honcho-cli/src/honcho_cli/main.py +++ b/honcho-cli/src/honcho_cli/main.py @@ -15,6 +15,7 @@ from honcho_cli import __version__ from honcho_cli._help import HonchoTyperGroup, print_welcome from honcho_cli.branding import BANNER from honcho_cli.output import set_json_mode +from honcho_cli.update_check import maybe_print_update_nag app = typer.Typer( @@ -60,6 +61,7 @@ def main( if ctx.invoked_subcommand is None: print_welcome(Console()) raise typer.Exit() + maybe_print_update_nag() # Register top-level commands diff --git a/honcho-cli/src/honcho_cli/update_check.py b/honcho-cli/src/honcho_cli/update_check.py new file mode 100644 index 00000000..97b433d1 --- /dev/null +++ b/honcho-cli/src/honcho_cli/update_check.py @@ -0,0 +1,67 @@ +"""Once-a-day stderr notice when a newer honcho-cli is on PyPI. + +Fail-open: any error is swallowed. Cache is ``update-check.json`` beside +config, not ``config.json``. +""" + +from __future__ import annotations + +import json +import os +import sys +import time + +import httpx + +from honcho_cli import __version__ +from honcho_cli.branding import ICON_RUN +from honcho_cli.config import _config_dir +from honcho_cli.output import console, use_json + +_INTERVAL_S = 24 * 60 * 60 +_PYPI_URL = "https://pypi.org/pypi/honcho-cli/json" + + +def maybe_print_update_nag() -> None: + if use_json() or "--json" in sys.argv: + return + if os.environ.get("HONCHO_NO_UPDATE_CHECK", "").lower() in ("1", "true"): + return + try: + path = _config_dir() / "update-check.json" + now = time.time() + try: + cache = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError, UnicodeDecodeError): + cache = {} + if isinstance(cache, dict) and now - float(cache.get("t") or 0) < _INTERVAL_S: + return + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({"t": now}), encoding="utf-8") + latest = httpx.get(_PYPI_URL, timeout=1.0).json()["info"]["version"] + if not isinstance(latest, str) or not _is_newer(latest, __version__): + return + console.print(f" {ICON_RUN} honcho-cli {latest} is available (you have {__version__})") + console.print(" [dim]uv tool upgrade honcho-cli[/dim]") + except Exception: + return + + +def _is_newer(latest: str, current: str) -> bool: + def parts(version: str) -> tuple[int, ...]: + out: list[int] = [] + for segment in version.lstrip("v").split("."): + num = "" + for ch in segment: + if ch.isdigit(): + num += ch + else: + break + if not num: + break + out.append(int(num)) + return tuple(out) or (0,) + + a, b = parts(latest), parts(current) + n = max(len(a), len(b)) + return a + (0,) * (n - len(a)) > b + (0,) * (n - len(b))