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
13 changes: 11 additions & 2 deletions docs/src/content/docs/reference/cli/deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ All subcommands operate on the project scope (`./apm_modules/`) by default. Pass

### `apm deps list`

List installed dependencies and the primitive counts each one contributes.
List every installed dependency recorded by the manifest or lockfile and the
primitive counts each one contributes. Manifests embedded anywhere inside an
installed package's source tree are parent-owned content, not separate
dependencies. Real lockfile-resolved dependencies install at their own package
roots and remain visible regardless of graph depth.

```bash
apm deps list [OPTIONS]
Expand All @@ -46,7 +50,12 @@ apm deps list [OPTIONS]

### `apm deps tree`

Render the dependency graph as a hierarchical tree, using `apm.lock.yaml` when present and falling back to a scan of `apm_modules/`.
Render the complete dependency graph as a hierarchical tree, following
`resolved_by` relationships at every lockfile depth. When no lockfile is
present, the command falls back to a scan of `apm_modules/` and ignores
parent-owned manifests embedded inside an installed package. Circular
relationships are marked `(circular)` at the repeated ancestor and do not
prevent other branches from rendering.

```bash
apm deps tree [OPTIONS]
Expand Down
20 changes: 16 additions & 4 deletions docs/src/content/docs/reference/cli/prune.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
---
title: apm prune
description: Remove orphaned packages no longer referenced in apm.yml
description: Remove packages absent from the resolved dependency graph
sidebar:
order: 7
---

Remove installed packages from `apm_modules/` that are no longer declared in `apm.yml`, along with their deployed integration files. Updates `apm.lock.yaml` to match.
Remove installed packages from `apm_modules/` that are neither declared in
`apm.yml` nor retained as transitive nodes in `apm.lock.yaml`. The command also
removes their deployed integration files and updates the lockfile.

## Synopsis

Expand All @@ -14,7 +16,10 @@ apm prune [--dry-run]
```

:::note[Coming from npm?]
`apm prune` mirrors `npm prune`: it removes installed packages that are not in your manifest. Unlike npm, it also removes the harness-deployed files those packages produced (prompts, agents, hooks, MCP wiring) and rewrites the lockfile.
`apm prune` mirrors `npm prune`: it removes installed packages that are not in
the manifest's resolved dependency graph. Unlike npm, it also removes the
harness-deployed files those packages produced (prompts, agents, hooks, MCP
wiring) and rewrites the lockfile.
:::

## Description
Expand All @@ -25,7 +30,12 @@ apm prune [--dry-run]
2. Packages installed under `apm_modules/`
3. Packages recorded in `apm.lock.yaml` with their `deployed_files`

Anything installed but not declared in either dependency list is **orphaned**. `apm prune` removes the orphan's directory under `apm_modules/`, deletes every file the orphan deployed into your harness directories (using the `deployed_files` manifest in the lockfile), removes the entry from `apm.lock.yaml`, and cleans up empty parent directories.
An installed package is **orphaned** when it is neither declared in either
dependency list nor retained as a lockfile-resolved transitive dependency.
`apm prune` removes the orphan's directory under `apm_modules/`, deletes every
file the orphan deployed into your harness directories (using the
`deployed_files` manifest in the lockfile), removes the entry from
`apm.lock.yaml`, and cleans up empty parent directories.

If `apm_modules/` does not exist, the command exits cleanly with nothing to do. If `apm.yml` is missing, it exits with an error.

Expand Down Expand Up @@ -70,6 +80,8 @@ For each orphaned package, `apm prune`:
Notes:

- Packages that share an install root with a still-declared sibling subdirectory dependency are not falsely protected by ancestor expansion. The check uses lockfile membership (with `apm.yml` fallback) to identify genuine standalone packages.
- A manifest embedded at any depth inside an installed package is owned by that
package. It is not an independent dependency, orphan, or prune candidate.
- Deploy paths are validated before deletion; entries that escape the project root are skipped.
- The command does not network. It only inspects local state.

Expand Down
6 changes: 3 additions & 3 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
| `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, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--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` (no package/deployment writes; a newly bootstrapped `apm.yml` and explicit targets are kept), `--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; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; zero matches fail with available names; persisted in apm.yml only on success; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--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 `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; `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, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2). Explicit plugin component paths must resolve inside the plugin root; missing declarations fail before deployment and lockfile commit. |
| `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 (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
| `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 | -- |
| `apm prune` | Remove installed packages absent from the manifest and lockfile-resolved graph | `--dry-run` |
| `apm deps list` | List manifest- and lockfile-resolved packages; ignore parent-owned embedded manifests | `-g` global, `--all` both scopes, `--insecure` |
| `apm deps tree` | Show the complete lockfile-resolved tree at any depth; mark repeated ancestors as circular | -- |
| `apm deps why PKG` | Explain why a package is installed (walks lockfile bottom-up to direct deps; analogue of `npm why` / `yarn why`) | `-g` global, `--json` |
| `apm find <PATH>` | Trace a deployed file back to the package(s) that contributed it (inverse of install; reads `apm.lock.yaml` only) | `--source` show OCI/git/local origin, `--path` show full why-chain (same as `apm deps why`) |
| `apm view PKG [FIELD]` | View package details, git refs, or registry versions | `-g` global, `FIELD=versions`, `--registry [NAME]` forces registry path for versions |
Expand Down
22 changes: 13 additions & 9 deletions src/apm_cli/commands/deps/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
def _scan_installed_packages(apm_modules_dir: Path) -> list:
"""Scan *apm_modules_dir* for installed package paths.

Walks the tree to find directories containing ``apm.yml`` or ``.apm``,
supporting GitHub (2-level), ADO (3-level), and subdirectory packages.
Walks the tree to find top-level directories containing ``apm.yml`` or
``.apm``, supporting GitHub (2-level), ADO (3-level), and subdirectory
packages. Package manifests nested below another package are part of that
parent package and are excluded.

Returns:
List of ``"owner/repo"`` or ``"org/project/repo"`` path keys.
Expand All @@ -25,6 +27,8 @@ def _scan_installed_packages(apm_modules_dir: Path) -> list:
continue
if not ((candidate / APM_YML_FILENAME).exists() or (candidate / APM_DIR).exists()):
continue
if _is_nested_under_package(candidate, apm_modules_dir):
continue
rel_parts = candidate.relative_to(apm_modules_dir).parts
if len(rel_parts) >= 2:
installed.append("/".join(rel_parts))
Expand All @@ -34,16 +38,16 @@ def _scan_installed_packages(apm_modules_dir: Path) -> list:
def _is_nested_under_package(candidate: Path, apm_modules_path: Path) -> bool:
"""Check if *candidate* is a sub-directory of another installed package.

When a plugin ships ``skills/*/SKILL.md`` at its root (outside ``.apm/``),
the ``rglob`` scan would otherwise treat each skill sub-directory as an
independent package. This helper walks up from *candidate* towards
*apm_modules_path* and returns ``True`` if any intermediate parent already
contains ``apm.yml`` -- meaning the candidate is a deployment artifact, not
a standalone package.
When a package ships nested package or skill manifests, the ``rglob`` scan
would otherwise treat each sub-directory as an independent package. This
helper walks up from *candidate* towards *apm_modules_path* and returns
``True`` if any intermediate parent already contains ``apm.yml`` or
``.apm`` -- meaning the candidate is a deployment artifact, not a standalone
package.
"""
parent = candidate.parent
while parent != apm_modules_path and parent != parent.parent: # noqa: PLR1714
if (parent / APM_YML_FILENAME).exists():
if (parent / APM_YML_FILENAME).exists() or (parent / APM_DIR).exists():
return True
parent = parent.parent
return False
Expand Down
Loading
Loading