Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@ apm update
- Downloads and runs the official platform installer (`install.sh` on macOS/Linux, `install.ps1` on Windows)
- Preserves existing configuration and projects
- Shows progress and success/failure status
- Some package-manager distributions can disable self-update at build time.
In those builds, `apm update` prints a distributor-defined guidance message
(for example, a `brew upgrade` command) and exits without running the installer.

**Version Checking:**
APM automatically checks for updates (at most once per day) when running any command. If a newer version is available, you'll see a yellow warning:
Expand All @@ -597,6 +600,8 @@ Run apm update to upgrade

This check is non-blocking and cached to avoid slowing down the CLI.

In distributions that disable self-update at build time, this startup update notification is skipped.

Comment thread
melund marked this conversation as resolved.
**Manual Update:**
If the automatic update fails, you can always update manually:

Expand Down
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@
| `apm config` | Show current configuration | -- |
| `apm config get [KEY]` | Get a config value | -- |
| `apm config set KEY VALUE` | Set a config value | -- |
| `apm update` | Update APM itself | `--check` only check |
| `apm update` | Update APM itself (or show distributor guidance when self-update is disabled at build time) | `--check` only check |
7 changes: 6 additions & 1 deletion src/apm_cli/commands/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
GITIGNORE_FILENAME,
)
from ..utils.console import _rich_echo, _rich_info, _rich_warning
from ..update_policy import get_update_hint_message, is_self_update_enabled
from ..version import get_build_sha, get_version
from ..utils.version_checker import check_for_updates

Expand Down Expand Up @@ -240,6 +241,10 @@ def print_version(ctx, param, value):
def _check_and_notify_updates():
"""Check for updates and notify user non-blockingly."""
try:
# Skip notifications when self-update is disabled by distribution policy.
if not is_self_update_enabled():
return

# Skip version check in E2E test mode to avoid interfering with tests
if os.environ.get("APM_E2E_TESTS", "").lower() in ("1", "true", "yes"):
return
Expand All @@ -260,7 +265,7 @@ def _check_and_notify_updates():
)

# Show update command using helper for consistency
_rich_echo("Run apm update to upgrade", color="yellow", bold=True)
_rich_echo(get_update_hint_message(), color="yellow", bold=True)

# Add a blank line for visual separation
click.echo()
Expand Down
6 changes: 6 additions & 0 deletions src/apm_cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import click

from ..core.command_logger import CommandLogger
from ..update_policy import get_self_update_disabled_message, is_self_update_enabled
from ..version import get_version


Expand Down Expand Up @@ -65,6 +66,11 @@ def update(check):
import tempfile

logger = CommandLogger("update")

if not is_self_update_enabled():
logger.warning(get_self_update_disabled_message())
return

current_version = get_version()

# Skip check for development versions
Expand Down
51 changes: 51 additions & 0 deletions src/apm_cli/update_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Build-time policy for APM self-update behavior.

Package maintainers can patch this module during build to disable self-update
and show users a package-manager-specific update command.
"""

# Default guidance when self-update is disabled.
DEFAULT_SELF_UPDATE_DISABLED_MESSAGE = (
"Self-update is disabled for this APM distribution. "
"Update APM using your package manager."
)

# Build-time policy values.
#
# Packagers can patch these constants during build, for example:
# - SELF_UPDATE_ENABLED = False
# - SELF_UPDATE_DISABLED_MESSAGE = "Update with: pixi update apm-cli"
SELF_UPDATE_ENABLED = True
SELF_UPDATE_DISABLED_MESSAGE = DEFAULT_SELF_UPDATE_DISABLED_MESSAGE


def _is_printable_ascii(value: str) -> bool:
"""Return True when value contains only printable ASCII characters."""
return all(" " <= char <= "~" for char in value)


def is_self_update_enabled() -> bool:
"""Return True when this build allows self-update."""
return SELF_UPDATE_ENABLED is True


def get_self_update_disabled_message() -> str:
"""Return the guidance message shown when self-update is disabled."""
if SELF_UPDATE_DISABLED_MESSAGE is None:
return DEFAULT_SELF_UPDATE_DISABLED_MESSAGE

message = str(SELF_UPDATE_DISABLED_MESSAGE).strip()
if not message:
return DEFAULT_SELF_UPDATE_DISABLED_MESSAGE

if not _is_printable_ascii(message):
return DEFAULT_SELF_UPDATE_DISABLED_MESSAGE

return message

Comment thread
melund marked this conversation as resolved.

def get_update_hint_message() -> str:
"""Return the update hint used in startup notifications."""
if is_self_update_enabled():
return "Run apm update to upgrade"
return get_self_update_disabled_message()
9 changes: 9 additions & 0 deletions tests/unit/test_command_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ def test_returns_empty_for_empty_dir(self, tmp_path):
class TestCheckAndNotifyUpdates:
"""Tests for _check_and_notify_updates."""

def test_skips_when_self_update_disabled(self):
"""Returns immediately when distribution disables self-update."""
with patch(
"apm_cli.commands._helpers.is_self_update_enabled", return_value=False
):
with patch("apm_cli.commands._helpers.check_for_updates") as mock_check:
_check_and_notify_updates()
mock_check.assert_not_called()

def test_skips_in_e2e_test_mode(self):
"""Returns immediately when APM_E2E_TESTS=1 is set."""
with patch.dict(os.environ, {"APM_E2E_TESTS": "1"}):
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/test_update_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ def test_manual_update_command_uses_windows_installer(self):
self.assertIn("aka.ms/apm-windows", command)
self.assertIn("powershell", command.lower())

@patch("apm_cli.commands.update.is_self_update_enabled", return_value=False)
@patch(
"apm_cli.commands.update.get_self_update_disabled_message",
return_value="Update with: pixi update apm-cli",
)
@patch("subprocess.run")
@patch("requests.get")
def test_update_command_respects_disabled_policy(
self,
mock_get,
mock_run,
mock_message,
mock_enabled,
):
"""Disabled self-update policy should print guidance and skip installer."""
result = self.runner.invoke(cli, ["update"])

self.assertEqual(result.exit_code, 0)
self.assertIn("Update with: pixi update apm-cli", result.output)
mock_get.assert_not_called()
mock_run.assert_not_called()

@patch("requests.get")
@patch("subprocess.run")
@patch("apm_cli.commands.update.get_version", return_value="0.6.3")
Expand Down
Loading