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

### Added

- `apm uninstall` now accepts the same marketplace notation as `apm install` (e.g. `my-plugin@official`) -- no more `owner/repo` lookup before removing a plugin you installed by name. Refs resolve via lockfile first (offline), then registry fallback, with a supply-chain guard that refuses any registry-returned canonical not already in the lockfile. ([#1323](https://github.com/microsoft/apm/issues/1323))
- 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)
Expand Down
26 changes: 23 additions & 3 deletions docs/src/content/docs/reference/cli/uninstall.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ The command only deletes files tracked in the lockfile's `deployed_files` manife

| Argument | Description |
|---|---|
| `PACKAGES...` | One or more packages to remove. Accepts shorthand (`owner/repo`), HTTPS URL, SSH URL, or FQDN. APM resolves each to the canonical identity stored in `apm.yml`. Required. |
| `PACKAGES...` | One or more packages to remove. Accepts shorthand (`owner/repo`), HTTPS URL, SSH URL, FQDN, or marketplace notation (`name@marketplace`). APM resolves each to the canonical identity stored in `apm.yml`. Required. |
Comment thread
sergio-sisternes-epam marked this conversation as resolved.

## Options

| Option | Description |
|---|---|
| `--dry-run` | Show what would be removed without touching disk. |
| `--dry-run` | Show what would be removed without touching disk. Registry fallback for marketplace notation is skipped. |
| `-v, --verbose` | Show detailed removal information. |
| `-g, --global` | Remove from the user scope (`~/.apm/`) instead of the current project. |

Expand Down Expand Up @@ -59,6 +59,12 @@ Remove from the user scope:
apm uninstall -g acme/my-package
```

Remove by marketplace name (resolved via lockfile, then registry):

```bash
apm uninstall my-plugin@official
```

Resolve via URL (same identity as the shorthand):

```bash
Expand All @@ -80,7 +86,21 @@ What gets removed, in order:

If a name passed on the command line is not found in `apm.yml`, the command warns and continues with the rest. If none of the names match, it exits without changes.

`--dry-run` runs steps 1-3 in memory and prints the plan; nothing is written.
If a marketplace ref cannot be resolved (neither the lockfile nor the registry has a matching entry), APM logs an error and skips that package. Use `owner/repo` notation to uninstall directly, or run `apm list` to find the canonical name.

### Supply-chain guard

When marketplace notation (`name@marketplace`) falls through to the registry (Stage 2), APM refuses any canonical the registry returns that is not already recorded in `apm.lock.yaml`. The refusal is reported as a warning naming the resolved canonical so you can decide whether to re-run with `apm uninstall owner/repo` directly. This prevents a poisoned marketplace registry from coercing APM into removing an unrelated installed package.

### `#ref` is not meaningful for `uninstall`

`apm install` accepts an optional `#ref` fragment (`apm install NAME@MKT#ref`) to pin a specific revision. `apm uninstall` identifies packages by canonical name only, so any `#ref` fragment supplied with marketplace notation (e.g. `my-plugin@official#v1.0.0`) is ignored.

### No-lockfile behavior

If `apm.lock.yaml` is not present, marketplace notation has no offline anchor: Stage 1 finds nothing, and the supply-chain guard cannot cross-check the registry result. APM still attempts registry resolution and proceeds if the canonical matches an entry in `apm.yml`, but this path has weaker integrity guarantees. Prefer `owner/repo` form when there is no lockfile, or run `apm install` to regenerate the lockfile first.

`--dry-run` runs steps 1-3 in memory and prints the plan; nothing is written. Registry fallback is also skipped in dry-run mode, so marketplace refs not already in the lockfile cannot be previewed; use `owner/repo` notation or re-run without `--dry-run`.

## Related

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 @@ -12,7 +12,7 @@
|---------|---------|-----------|
| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `--target all` deprecated, see `apm compile --all`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`), `--dev`, `-g` global, `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from SKILL_BUNDLE (repeatable; persisted in apm.yml; `'*'` resets to all), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry |
| `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). |
| `apm uninstall PKGS...` | Remove packages | `--dry-run`, `-g` global |
| `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
| `apm prune` | Remove orphaned packages | `--dry-run` |
| `apm deps list` | List installed packages | `-g` global, `--all` both scopes, `--insecure` |
| `apm deps tree` | Show dependency tree | -- |
Expand Down
20 changes: 14 additions & 6 deletions src/apm_cli/commands/uninstall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def uninstall(ctx, packages, dry_run, verbose, global_):
apm uninstall org/pkg1 org/pkg2 # Remove multiple packages
apm uninstall acme/my-package --dry-run # Show what would be removed
apm uninstall -g acme/my-package # Remove from user scope
apm uninstall my-plugin@official # Remove by marketplace name
"""
from ...core.scope import (
InstallScope,
Expand Down Expand Up @@ -99,9 +100,20 @@ def uninstall(ctx, packages, dry_run, verbose, global_):

current_deps = data["dependencies"]["apm"] or []

# Load lockfile early: used for marketplace ref resolution in Step 1
# and reused for MCP state capture and transitive orphan cleanup below.
from ...deps.lockfile import LockFile, get_lockfile_path

lockfile_path = get_lockfile_path(apm_dir)
lockfile = LockFile.read(lockfile_path)

# Step 1: Validate packages
from ...core.auth import AuthResolver

# Lazy: only construct the resolver when we will actually call the registry.
auth_resolver = None if dry_run else AuthResolver()
packages_to_remove, packages_not_found = _validate_uninstall_packages(
packages, current_deps, logger
packages, current_deps, logger, lockfile, auth_resolver=auth_resolver, dry_run=dry_run
)
if not packages_to_remove:
logger.warning("No packages found in apm.yml to remove")
Expand All @@ -125,11 +137,7 @@ def uninstall(ctx, packages, dry_run, verbose, global_):
logger.error(f"Failed to write {apm_yml_path}: {e}")
sys.exit(1)

# Step 4: Load lockfile and capture pre-uninstall MCP state
from ...deps.lockfile import LockFile, get_lockfile_path

lockfile_path = get_lockfile_path(apm_dir)
lockfile = LockFile.read(lockfile_path)
# Step 4: Capture pre-uninstall MCP state (lockfile already read above)
_pre_uninstall_mcp_servers = (
builtins.set(lockfile.mcp_servers) if lockfile else builtins.set()
)
Expand Down
Loading
Loading