Skip to content

Promote apm marketplace add to top-level apm add (and remove) #1075

@danielmeppiel

Description

@danielmeppiel

TL;DR

Promote apm marketplace add to the top-level apm add (and apm marketplace remove to apm remove). The most-typed onboarding command shrinks from 3 tokens to 2, aligning APM with cargo add, npm install, gh extension install, brew install. The legacy apm marketplace add/remove keeps working with a one-line stderr tip; no breaking change.

Problem (WHY)

The README quickstart for marketplaces today reads:

apm marketplace add github/awesome-copilot
apm install azure-cloud-development@awesome-copilot

That's two commands and the first one buries the action verb (add) under a noun-group (marketplace) the user does not think about. Every successful package manager puts its source-management verb at depth 0:

Tool Verb Depth
npm npm install <pkg> 0
cargo cargo add <crate> 0
pip pip install <pkg> 0
brew brew install <formula> 0
gh CLI gh extension install OWNER/REPO 1
Claude Code /plugin marketplace add OWNER/REPO 2 (slash UI)
APM (current) apm marketplace add OWNER/REPO 1
APM (proposed) apm add OWNER/REPO 0

Friction this creates:

  • Tutorials, blog posts, and screencasts have to spell marketplace -- 11 chars of cognitive overhead before the verb.
  • New users have to learn the noun "marketplace" before they can register their first source. The noun is plumbing, not user-facing.
  • README install snippet is 3 tokens vs the 2-token snippet every developer's muscle memory expects.

There's already a precedent in the codebase: apm search was promoted from apm marketplace search to top-level (see src/apm_cli/cli.py:92). This issue extends that pattern to the highest-frequency consumer verbs.

Proposed Solution (WHAT)

New top-level commands

apm add OWNER/REPO [OWNER/REPO ...]      # NEW. Aliases `apm marketplace add`.
apm remove NAME                          # NEW. Aliases `apm marketplace remove`.

Backwards compatibility

  • apm marketplace add and apm marketplace remove keep working unchanged.
  • On successful invocation, the legacy command emits ONE line to stderr:
    [i] Tip: 'apm add' is now available as a top-level command. Try: apm add OWNER/REPO
    
  • apm marketplace add --help text annotates "(alias: apm add)".
  • No hard removal pre-1.0. No deprecation timeline.

Multi-source support (cargo parity)

apm add acme/skills contoso/security-plugins

Sources are processed left-to-right with continue-on-error for non-security failures (404, network timeouts), and fail-closed on security-class failures (signature mismatch, path-segment violation, integrity check). Exit code is non-zero if any source failed; a summary line is printed:

[*] Registering marketplace 'skills' (12 plugins)        -> success
[x] Failed to register 'bad/repo': No marketplace.json found
[*] Registering marketplace 'tools' (8 plugins)          -> success

Summary: 2 registered, 1 failed

Smart error: bare name typo

If user types apm add cool-plugin (no /):

[x] Invalid source: 'cool-plugin'. Expected OWNER/REPO format.

    Did you mean `apm install cool-plugin`?
    Use `apm add OWNER/REPO` to register a marketplace source.

Exit code: 1.

Note: Per supply-chain review, the apm install cool-plugin suggestion echoes the user input verbatim and does NOT imply the package exists or is trusted. Consider hardening this further (e.g. only suggest apm install when the bare name resolves against an already-registered source) as a follow-up.

Implementation Sketch (HOW)

Thin Click wrapper, no logic duplication. The architect lens proposes:

  1. Extract the existing add() body (src/apm_cli/commands/marketplace/__init__.py:247-403) and remove() body (lines 572-605) into a new private module src/apm_cli/commands/marketplace/_source_ops.py with two functions: _do_add_source(repos, name, branch, host, verbose, invoked_as_legacy) and _do_remove_source(name, yes, verbose, invoked_as_legacy).

  2. Replace the marketplace add and marketplace remove Click bodies with calls to those helpers, passing invoked_as_legacy=True.

  3. Create src/apm_cli/commands/source.py defining top-level add and remove Click commands that call the same helpers with invoked_as_legacy=False.

  4. Wire the new commands into src/apm_cli/cli.py immediately after install/uninstall so they render adjacently in --help.

  5. Deprecation tip is emitted only when invoked_as_legacy=True AND the operation succeeds. Uses the existing _rich_info helper with [i] symbol; routed to stderr via a small extension to _rich_echo accepting a file= kwarg, OR a plain click.echo(..., err=True) fallback if the helper extension is deferred.

  6. Help-text reorg (apm --help): out of scope for this PR. Promoting add/remove adjacent to install/uninstall in registration order is enough; full categorization is a separate issue once we have 3+ promoted verbs (precedent: MarketplaceGroup.format_commands).

Files touched

File Change
src/apm_cli/commands/marketplace/_source_ops.py NEW. Pure logic.
src/apm_cli/commands/marketplace/__init__.py add / remove bodies become 1-line calls.
src/apm_cli/commands/source.py NEW. Top-level Click commands.
src/apm_cli/cli.py Register add, remove after install/uninstall.
src/apm_cli/utils/console.py Optional: extend _rich_echo with file= kwarg.

Acceptance Criteria

  • apm add OWNER/REPO registers a marketplace source (same outcome as apm marketplace add OWNER/REPO).
  • apm add A/B C/D registers multiple sources; partial failure yields exit code 1 with a summary line.
  • Security-class failures (signature, integrity, path traversal) abort the entire batch and return non-zero immediately. Non-security failures (404, network) are skipped with continue-on-error semantics.
  • apm remove NAME removes a registered marketplace.
  • apm marketplace add and apm marketplace remove still work; each prints exactly one stderr tip line on success, never on error.
  • apm add cool-plugin (no slash) exits 1 with the smart-error message.
  • apm add --name foo a/b c/d errors: --name requires a single source.
  • apm add --help shows OWNER/REPO [OWNER/REPO ...] usage.
  • apm marketplace add --help includes "(alias: apm add)".
  • No emoji or non-ASCII in any output path (per .github/instructions/encoding.instructions.md).
  • Existing tests/unit/marketplace/test_marketplace_commands.py tests pass unchanged (logic extracted, behavior preserved).
  • New tests/unit/commands/test_source_commands.py covers: single source, multi-source success, multi-source partial failure, security-class fail-closed, bare-name smart error, --name + multi conflict, deprecation tip presence/absence.
  • uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ pass clean.
  • CHANGELOG.md [Unreleased] -> Added entry.

Docs Checklist (in same PR)

  • README.md -- swap quickstart marketplace example to apm add github/awesome-copilot; one-line note on long form.
  • docs/src/content/docs/reference/cli-commands.md -- add apm add and apm remove reference entries; annotate apm marketplace add/remove headings with "(alias: apm add/remove)"; add rows to the package-commands table.
  • docs/src/content/docs/guides/marketplaces.md -- swap primary register/remove examples to apm add/apm remove; one inline note explains alias relationship.
  • docs/src/content/docs/guides/marketplace-authoring.md -- update the consumer-facing line to apm add <owner>/<repo> (long form once in parens).
  • docs/src/content/docs/enterprise/registry-proxy.md -- update proxy compatibility table cell to include apm add.
  • packages/apm-guide/.apm/skills/apm-usage/commands.md -- add canonical rows for apm add OWNER/REPO and apm remove NAME; mark apm marketplace add|remove as long form so the skill teaches apm add first.
  • Add :::note admonitions under the new apm add/apm remove reference headings clarifying the alias relationship.

Out of Scope (file separately)

  • Promoting apm marketplace list to a top-level apm list --sources (or similar). Its UX questions (filtering, output format) deserve their own discussion. The CEO lens explicitly recommended scoping this issue to add + remove only.
  • Help-text categorization (Click group reorg into "Package commands" / "Project commands" / "Advanced" sections). Lift MarketplaceGroup.format_commands pattern to root in a follow-up once 3+ verbs are promoted.
  • Hardening the smart-error suggestion to only propose apm install <name> when <name> resolves against an already-registered source. Tracked as supply-chain follow-up.

Strategic Rationale

"File it. apm add OWNER/REPO is the single highest-leverage CLI ergonomics change available: it collapses the most-typed onboarding command from three tokens to two, aligns APM's command vocabulary with every peer package manager users already know, and costs near-zero implementation risk (thin wrapper, existing tests untouched). The soft deprecation cadence respects contributors and scripts already in the wild." -- apm-ceo

"Safe as-is because it delegates to validated code. Conditions: smart-error suggestions must not endorse arbitrary user input as a known package; continue-on-error must treat security-class failures as batch-aborting, not skippable." -- supply-chain-security-expert

References

Orchestration Provenance

This issue was synthesized via the genesis-skill orchestration pattern: parallel fan-out across oss-growth-hacker + devx-ux-expert (research wave), fused python-architect + cli-logging-ux for design, doc-writer for docs delta, parallel sanity gate from supply-chain-security-expert + apm-ceo. Both gate verdicts incorporated in Acceptance Criteria.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cliCLI command surface, flags, help text (cross-cutting).area/marketplacemarketplace.json schema, federation, authoring suite, source parity.cliDeprecated: use area/cli. Kept for issue history; will be removed in milestone 0.10.0.enhancementDeprecated: use type/feature. Kept for issue history; will be removed in milestone 0.10.0.priority/highShips in current or next milestonestatus/acceptedDirection approved, safe to start work.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/portabilityOne manifest, every target. Multi-target deploy, marketplace, packaging, install.type/featureNew capability, new flag, new primitive.

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions