Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added `--target/-t` option to `apm update` command to specify agent target (#1297)
- `apm pack --marketplace=FORMATS` filters which marketplace formats are built in a single run; accepts comma-separated names and sentinels `all`/`none`. (#1317)
- `apm pack --marketplace-path FORMAT=PATH` overrides the output path for a specific marketplace format at invocation time. Env var overrides (`APM_MARKETPLACE_<FORMAT>_PATH`) are planned for v0.15. (#1317)
- `apm pack --json` emits a stable JSON contract to stdout (`{ok, dry_run, warnings, errors, marketplace: {outputs: [{format, path, ...}]}}`); all logs move to stderr so downstream tooling can `jq` the output safely. (#1317)
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/cli/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For a read-only install that pins to whatever is already in `apm.lock.yaml` -- t
| `--yes`, `-y` | off | Skip the interactive prompt and accept the plan. Required for non-interactive use. |
| `--dry-run` | off | Compute and print the plan without prompting and without writing the lockfile or filesystem. |
| `--verbose`, `-v` | off | Show per-dependency resolution detail (old ref, new ref, source) and full error context. |
| `--target TARGET`, `-t TARGET` | auto-detect | Agent harness(es) to update for. Accepts a single value (`claude`, `copilot`, `cursor`, `windsurf`, `codex`, `opencode`, `gemini`) or comma-separated list (`--target claude,cursor`). Overrides `apm.yml targets:` and auto-detection. |

## Examples

Expand Down
34 changes: 34 additions & 0 deletions src/apm_cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
* ``--dry-run`` -- render the plan and exit without prompting.
* ``--verbose``/``-v`` -- show unchanged deps in the plan and pipeline
diagnostics.
* ``--target``/``-t`` -- agent harness(es) to deploy to (e.g.
``claude``, ``copilot``, ``cursor``, ``windsurf``, ``codex``,
``opencode``, ``gemini``); comma-separated for multiple targets.
Overrides ``apm.yml targets:`` and auto-detection.

Other ``apm install`` flags are NOT mirrored here on purpose -- the
update command stays focused on the refresh-and-confirm loop.
Expand All @@ -43,6 +47,7 @@
import click

from ..core.command_logger import InstallLogger
from ..core.target_detection import TargetParamType
from ..install.errors import (
AuthenticationError,
DirectDependencyError,
Expand Down Expand Up @@ -121,13 +126,27 @@ def _stdin_is_tty() -> bool:
help="(Deprecated) Forwarded to 'apm self-update --check' when run outside an apm.yml project; rejected inside a project.",
hidden=True,
)
@click.option(
"--target",
"-t",
type=TargetParamType(),
default=None,
help=(
"Agent target(s) to update for "
"(e.g. claude, copilot, cursor, windsurf, codex, opencode, gemini). "
"Comma-separated for multiple: --target claude,cursor. "
"Highest-priority entry in the resolution chain "
"(--target > apm.yml targets: > auto-detect)."
),
)
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
@click.pass_context
def update(
ctx: click.Context,
assume_yes: bool,
dry_run: bool,
verbose: bool,
check_only: bool,
target: str | list[str] | None,
) -> None:
"""Refresh APM dependencies to the latest matching refs.
Comment thread
sergio-sisternes-epam marked this conversation as resolved.

Expand All @@ -145,6 +164,12 @@ def update(
# the release after this one.
from apm_cli.commands.self_update import self_update as _self_update_cmd

if target is not None:
_rich_warning(
"--target is ignored when forwarding to 'apm self-update' "
"(no apm.yml found). Use 'apm self-update' directly.",
symbol="warning",
)
_rich_warning(
"'apm update' refreshes APM dependencies. To update the CLI binary, "
"use 'apm self-update'. Forwarding for back-compat (deprecated).",
Expand All @@ -156,6 +181,12 @@ def update(
if check_only:
from apm_cli.commands.self_update import self_update as _self_update_cmd

if target is not None:
_rich_warning(
"--target is ignored when forwarding to 'apm self-update --check'. "
"Use 'apm update --dry-run' to preview dependency changes.",
symbol="warning",
)
_rich_warning(
"'apm update --check' is the deprecated self-updater shim. "
"Use 'apm update --dry-run' to preview dependency changes, "
Expand All @@ -178,6 +209,7 @@ def update(
dry_run=dry_run,
verbose=verbose,
project_root=project_root,
target=target,
)


Expand All @@ -187,6 +219,7 @@ def _run_dep_update(
dry_run: bool,
verbose: bool,
project_root: Path | None = None,
target: str | list[str] | None = None,
) -> None:
"""Core ``apm update`` flow: resolve, plan, prompt, install.

Expand Down Expand Up @@ -282,6 +315,7 @@ def _plan_callback(plan: UpdatePlan) -> bool:
scope=InstallScope.PROJECT,
logger=logger,
plan_callback=_plan_callback,
target=target,
)
except FrozenInstallError as e:
_rich_error(str(e))
Expand Down
133 changes: 133 additions & 0 deletions tests/unit/commands/test_update_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,139 @@ def test_update_without_apm_yml_forwards_to_self_update(self, runner, tmp_path):
assert mock_self_update.called


# -----------------------------------------------------------------------------
# apm update --target flag
# -----------------------------------------------------------------------------


class TestUpdateTarget:
def test_target_forwarded_to_install(self, runner, tmp_path):
"""--target value is passed through to _install_apm_dependencies."""
with runner.isolated_filesystem(temp_dir=tmp_path):
_make_apm_yml(Path.cwd())
captured = {}

def fake_install(_apm, **kwargs):
captured["target"] = kwargs.get("target")
cb = kwargs["plan_callback"]
cb(UpdatePlan(entries=()))
from apm_cli.models.results import InstallResult

return InstallResult()

with patch(
"apm_cli.commands.install._install_apm_dependencies", side_effect=fake_install
):
result = runner.invoke(cli, ["update", "--target", "claude"])

assert result.exit_code == 0, result.output
assert captured["target"] == "claude"

def test_short_target_flag(self, runner, tmp_path):
"""-t short form is accepted and forwarded."""
with runner.isolated_filesystem(temp_dir=tmp_path):
_make_apm_yml(Path.cwd())
captured = {}

def fake_install(_apm, **kwargs):
captured["target"] = kwargs.get("target")
cb = kwargs["plan_callback"]
cb(UpdatePlan(entries=()))
from apm_cli.models.results import InstallResult

return InstallResult()

with patch(
"apm_cli.commands.install._install_apm_dependencies", side_effect=fake_install
):
result = runner.invoke(cli, ["update", "-t", "copilot"])

assert result.exit_code == 0, result.output
assert captured["target"] == "copilot"

Comment thread
sergio-sisternes-epam marked this conversation as resolved.
def test_no_target_defaults_to_none(self, runner, tmp_path):
"""Omitting --target passes None to _install_apm_dependencies."""
with runner.isolated_filesystem(temp_dir=tmp_path):
_make_apm_yml(Path.cwd())
captured = {}

def fake_install(_apm, **kwargs):
captured["target"] = kwargs.get("target")
cb = kwargs["plan_callback"]
cb(UpdatePlan(entries=()))
from apm_cli.models.results import InstallResult

return InstallResult()

with patch(
"apm_cli.commands.install._install_apm_dependencies", side_effect=fake_install
):
result = runner.invoke(cli, ["update"])

assert result.exit_code == 0, result.output
assert captured["target"] is None

def test_target_with_assume_yes(self, runner, tmp_path):
"""--target and --yes work together; target is forwarded and install proceeds."""
with runner.isolated_filesystem(temp_dir=tmp_path):
_make_apm_yml(Path.cwd())
captured = {}

def fake_install(_apm, **kwargs):
captured["target"] = kwargs.get("target")
cb = kwargs["plan_callback"]
captured["proceeded"] = cb(_stub_plan_with_changes())
from apm_cli.models.results import InstallResult

return InstallResult(installed_count=1)

with patch(
"apm_cli.commands.install._install_apm_dependencies", side_effect=fake_install
):
result = runner.invoke(cli, ["update", "--yes", "--target", "cursor"])

assert result.exit_code == 0, result.output
assert captured["target"] == "cursor"
assert captured["proceeded"] is True

def test_multi_target_comma_separated(self, runner, tmp_path):
"""--target claude,cursor (comma-separated) is parsed to a list and forwarded."""
with runner.isolated_filesystem(temp_dir=tmp_path):
_make_apm_yml(Path.cwd())
captured = {}

def fake_install(_apm, **kwargs):
captured["target"] = kwargs.get("target")
cb = kwargs["plan_callback"]
cb(UpdatePlan(entries=()))
from apm_cli.models.results import InstallResult

return InstallResult()

with patch(
"apm_cli.commands.install._install_apm_dependencies", side_effect=fake_install
):
result = runner.invoke(cli, ["update", "--target", "claude,cursor"])

assert result.exit_code == 0, result.output
assert isinstance(captured["target"], list), (
f"Expected list for multi-target, got {type(captured['target'])}"
)
assert "claude" in captured["target"]
assert "cursor" in captured["target"]

def test_target_ignored_warning_on_shim_path(self, runner, tmp_path):
"""--target outside an apm.yml project emits a warning that it will be ignored."""
with runner.isolated_filesystem(temp_dir=tmp_path):
with patch("apm_cli.commands.self_update.self_update.callback") as mock_self_update:
mock_self_update.return_value = None
result = runner.invoke(cli, ["update", "--target", "claude"])

assert "ignored" in result.output.lower() or "warning" in result.output.lower(), (
f"Expected an ignored/warning message, got: {result.output}"
)


# -----------------------------------------------------------------------------
# apm install --frozen / --update mutex
# -----------------------------------------------------------------------------
Expand Down
Loading