Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/04_automation_and_utility.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
log:
2026-06-01: Improved `colab update` output. On Linux platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
2026-06-01: Enabled `colab update --install` self-update on macOS in addition to Linux. Refactored platform check logic to keep the implementation DRY and updated both tests and documentation. Also, on these platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
2026-05-27: Refactored `colab README` and `colab AGENT` to bundle `README.md` and `AGENTS.md` via Hatchling's `force-include` and read them using `importlib.resources` instead of `importlib.metadata`. `colab AGENT` now correctly prints `AGENTS.md`.
2026-05-27: Extended `colab update --install` to detect if the CLI was installed via `uv tool install` (by checking if `sys.executable` contains `/uv/`) and if so, use `uv tool install -U google-colab-cli` to upgrade.
2026-05-27: Updated auto-update upgrade hint to recommend `pip install --upgrade google-colab-cli` instead of `colab`, aligning with the PyPI package name.
Expand Down Expand Up @@ -186,12 +186,12 @@ remediation guidance) rather than silently after ~1 minute via the daemon.
the cache.
- **Notification**: If a new version is found, a non-intrusive message is
printed to the console with a `Run 'pip install --upgrade google-colab-cli' to
update.` hint. On Linux platforms where `--install` self-update is supported,
update.` hint. On Linux and macOS platforms where `--install` self-update is supported,
an additional hint `You can run 'colab update --install' to upgrade in place.`
is displayed above the pip/uv install command. The cached banner shown between
fetches uses the generic `Run 'colab update' to update.` hint.
- **Self-install (`--install`)**: An opt-in `--install` flag (default
`False`) makes `colab update` upgrade the CLI in place (**Linux only**).
`False`) makes `colab update` upgrade the CLI in place (**Linux and macOS**).
It detects how the CLI was installed:
- If `sys.executable` contains `/uv/tools` (indicating it was installed via
`uv tool install`), it runs `uv tool install -U google-colab-cli`.
Expand Down
9 changes: 7 additions & 2 deletions src/colab_cli/auto_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
# ---------- Version detection -------------------------------------------


def is_self_install_supported() -> bool:
"""Return True if self-install (--install) is supported on the current platform."""
return platform.system() in ("Linux", "Darwin")


def get_app_version() -> str:
"""Return the installed package version, falling back to the git short hash."""
try:
Expand Down Expand Up @@ -113,7 +118,7 @@ def announce_upgrade(
typer.echo(
f"\n[colab] A new version of Colab CLI is available: {latest} (current: {current})"
)
if platform.system() == "Linux" and ("pip" in install_cmd or "uv" in install_cmd):
if is_self_install_supported() and ("pip" in install_cmd or "uv" in install_cmd):
typer.echo("[colab] You can run 'colab update --install' to upgrade in place.")
typer.echo(f"[colab] Run '{install_cmd}' to update.")
if show_disable_hint:
Expand All @@ -132,7 +137,7 @@ def _get_install_command() -> str:
"""Return the recommended installation command based on the environment."""
import sys

if platform.system() == "Linux" and "/uv/tools/" in sys.executable:
if is_self_install_supported() and "/uv/tools/" in sys.executable:
return f"uv tool install -U {PYPI_PACKAGE_NAME}"
return f"pip install --upgrade {PYPI_PACKAGE_NAME}"

Expand Down
6 changes: 3 additions & 3 deletions src/colab_cli/commands/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import platform
from typing import Optional

import typer
Expand Down Expand Up @@ -362,9 +361,10 @@ def update_command(
if not install:
return

if platform.system() != "Linux":
if not auto_update.is_self_install_supported():
typer.echo(
"[colab] '--install' self-install is only supported on Linux.", err=True
"[colab] '--install' self-install is only supported on Linux and macOS.",
err=True,
)
raise typer.Exit(code=1)

Expand Down
62 changes: 53 additions & 9 deletions tests/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def test_pypi_upgrade_uses_uv_hint(mocker, app_version, fake_settings, mock_pypi
assert idx_install < idx_uv


def test_pypi_upgrade_uses_pip_hint_non_linux(
def test_pypi_upgrade_uses_pip_hint_macos(
mocker, app_version, fake_settings, mock_pypi
):
app_version("1.0.0")
Expand All @@ -172,6 +172,30 @@ def test_pypi_upgrade_uses_pip_hint_non_linux(
mocker.patch("sys.executable", "/usr/bin/python")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Darwin")

result = runner.invoke(app, ["update"])
assert result.exit_code == 0
assert "available: 1.1.0 (current: 1.0.0)" in result.output
assert "You can run 'colab update --install' to upgrade in place." in result.output
assert "Run 'pip install --upgrade google-colab-cli' to update." in result.output

idx_install = result.output.find(
"You can run 'colab update --install' to upgrade in place."
)
idx_pip = result.output.find(
"Run 'pip install --upgrade google-colab-cli' to update."
)
assert idx_install < idx_pip


def test_pypi_upgrade_uses_pip_hint_windows(
mocker, app_version, fake_settings, mock_pypi
):
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("sys.executable", "/usr/bin/python")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Windows")

result = runner.invoke(app, ["update"])
assert result.exit_code == 0
assert "available: 1.1.0 (current: 1.0.0)" in result.output
Expand Down Expand Up @@ -448,7 +472,7 @@ def test_install_flag_runs_pip_install_upgrade(
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
mocker.patch("sys.executable", "/usr/bin/python")
run = mocker.patch(
"colab_cli.auto_update.subprocess.run",
Expand All @@ -472,7 +496,7 @@ def test_install_flag_runs_uv_tool_install(
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
mocker.patch(
"sys.executable", "/home/user/.local/share/uv/tools/google-colab-cli/bin/python"
)
Expand All @@ -489,21 +513,41 @@ def test_install_flag_runs_uv_tool_install(
assert cmd == ["uv", "tool", "install", "-U", "google-colab-cli"]


def test_install_flag_errors_on_non_linux(
def test_install_flag_errors_on_unsupported_platform(
mocker, app_version, fake_settings, mock_pypi
):
"""`--install` is gated to Linux; on other platforms the command must
"""`--install` is gated to Linux and macOS; on other platforms the command must
exit non-zero with an explanatory message and skip the pip subprocess."""
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Darwin")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Windows")
run = mocker.patch("colab_cli.auto_update.subprocess.run")

result = runner.invoke(app, ["update", "--install"])
assert result.exit_code != 0
assert run.call_count == 0
assert "only supported on Linux" in result.output
assert "only supported on Linux and macOS" in result.output


def test_install_flag_runs_on_macos(mocker, app_version, fake_settings, mock_pypi):
"""`colab update --install` shells out to pip/uv when running on macOS."""
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.auto_update.platform.system", return_value="Darwin")
mocker.patch("sys.executable", "/usr/bin/python")
run = mocker.patch(
"colab_cli.auto_update.subprocess.run",
return_value=mocker.Mock(returncode=0),
)

result = runner.invoke(app, ["update", "--install"])
assert result.exit_code == 0
assert run.call_count == 1
args, _ = run.call_args
cmd = args[0]
assert cmd == ["/usr/bin/python", "-m", "pip", "install", "-U", "google-colab-cli"]


def test_install_flag_no_op_when_already_up_to_date(
Expand All @@ -514,7 +558,7 @@ def test_install_flag_no_op_when_already_up_to_date(
app_version("1.1.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
run = mocker.patch("colab_cli.auto_update.subprocess.run")

result = runner.invoke(app, ["update", "--install"])
Expand All @@ -529,7 +573,7 @@ def test_install_flag_propagates_pip_failure(
app_version("1.0.0")
mock_pypi({"info": {"version": "1.1.0"}})
fake_settings()
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
mocker.patch(
"colab_cli.auto_update.subprocess.run",
return_value=mocker.Mock(returncode=2),
Expand Down