Skip to content

feat(deps): apm deps why <pkg> - explain why a transitive dependency was installed #1490

@danielmeppiel

Description

@danielmeppiel

Summary

Add apm deps why <pkg> — a subcommand that explains why a given
installed package is present, walking the dependency graph back to one
or more direct dependencies that pulled it in. Inspired by
npm why, yarn why, and cargo tree -i.

Motivation

After running apm install, apm_modules/ may contain transitive
packages that the consumer did not declare directly. When something
breaks — an unexpected MCP server, a skill that surprises the author,
a version that triggers a policy block — the consumer needs to know
who pulled it in. Today the only way to find out is to read
apm.lock.yaml by hand and trace resolved_by chains.

apm deps tree shows the graph top-down. apm deps why <pkg> shows
it bottom-up — the inverted view that answers "how did this get here?"

Proposal

A new subcommand under the existing apm deps group:

apm deps why <pkg> [--global] [--json]

<pkg> accepts the same identifier styles as apm deps info:
owner/repo, full URL, package alias, or marketplace name. The
lockfile is the source of truth — the command does not refetch
remotes.

Output (human-readable, default):

acme-org/shared-utils@1.4.2  (transitive)

  acme-org/big-skills@1.2.4   [constraint: ^1.2.0, declared in apm.yml]
   +-- acme-org/shared-utils@1.4.2   [constraint: ^1.4.0]

  acme-org/other-skills@0.9.1   [constraint: ^0.9.0, declared in apm.yml]
   +-- acme-org/shared-utils@1.4.2   [constraint: ~1.4.0]

When the target IS a direct dependency:

acme-org/big-skills@1.2.4  (direct dependency)

  Declared in apm.yml as: acme-org/big-skills#^1.2.0

When the package is not installed:

Error: 'acme-org/missing-pkg' is not in apm.lock.yaml.
Hint: run 'apm deps list' to see installed packages.

JSON output (--json) for scripting:

{
  "package": {
    "repo_url": "acme-org/shared-utils",
    "version": "1.4.2",
    "source": "git",
    "is_direct": false
  },
  "paths": [
    {
      "chain": [
        {"repo_url": "acme-org/big-skills", "constraint": "^1.2.0", "is_direct": true},
        {"repo_url": "acme-org/shared-utils", "constraint": "^1.4.0"}
      ]
    },
    {
      "chain": [
        {"repo_url": "acme-org/other-skills", "constraint": "^0.9.0", "is_direct": true},
        {"repo_url": "acme-org/shared-utils", "constraint": "~1.4.0"}
      ]
    }
  ]
}

Examples

  • apm deps why shared-utils — resolve by basename when unambiguous.
  • apm deps why acme-org/shared-utils — canonical form.
  • apm deps why acme-org/shared-utils --json — script-friendly.
  • apm deps why shared-utils --global — user-scope lookup.

Considerations

  • The lockfile already records resolved_by (parent repo URL) per
    dep. The constraint on each edge is not always recorded today;
    feat(deps): resolve semver constraints on git-source dependencies against repo tags #1488 adds the constraint field on LockedDependency. Without
    it, why falls back to displaying just the resolved version.
  • Ambiguous basename resolution: if shared-utils matches two
    packages from different owners, list all matches and prompt for
    the canonical form.
  • Performance: the lockfile is small (typically <100 deps). A full
    parent-chain BFS is microseconds; no caching needed.

Out of scope

  • Filtering by source (--source git, --source registry).
  • Highlighting policy violations on the chain (separate feature).
  • Live re-resolution from remotes; this command is offline by design.

References

  • Existing dep tree implementation: src/apm_cli/commands/deps/cli.py
    (_build_dep_tree, _add_tree_children).
  • Lockfile parent-chain field: LockedDependency.resolved_by.
  • Inspirations: npm why, yarn why, cargo tree -i.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cliCLI command surface, flags, help text (cross-cutting).area/package-authoringapm pack/unpack, plugin authoring, vendoring guidance, bundle format.status/needs-triageNew, awaiting maintainer review.type/featureNew capability, new flag, new primitive.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions