You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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:
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).
Replace the marketplace add and marketplace remove Click bodies with calls to those helpers, passing invoked_as_legacy=True.
Createsrc/apm_cli/commands/source.py defining top-level add and remove Click commands that call the same helpers with invoked_as_legacy=False.
Wire the new commands into src/apm_cli/cli.py immediately after install/uninstall so they render adjacently in --help.
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.
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.
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
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.
TL;DR
Promote
apm marketplace addto the top-levelapm add(andapm marketplace removetoapm remove). The most-typed onboarding command shrinks from 3 tokens to 2, aligning APM withcargo add,npm install,gh extension install,brew install. The legacyapm marketplace add/removekeeps working with a one-line stderr tip; no breaking change.Problem (WHY)
The README quickstart for marketplaces today reads:
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:npm install <pkg>cargo add <crate>pip install <pkg>brew install <formula>gh extension install OWNER/REPO/plugin marketplace add OWNER/REPOapm marketplace add OWNER/REPOapm add OWNER/REPOFriction this creates:
marketplace-- 11 chars of cognitive overhead before the verb.There's already a precedent in the codebase:
apm searchwas promoted fromapm marketplace searchto top-level (seesrc/apm_cli/cli.py:92). This issue extends that pattern to the highest-frequency consumer verbs.Proposed Solution (WHAT)
New top-level commands
Backwards compatibility
apm marketplace addandapm marketplace removekeep working unchanged.apm marketplace add --helptext annotates "(alias:apm add)".Multi-source support (cargo parity)
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:
Smart error: bare name typo
If user types
apm add cool-plugin(no/):Exit code: 1.
Implementation Sketch (HOW)
Thin Click wrapper, no logic duplication. The architect lens proposes:
Extract the existing
add()body (src/apm_cli/commands/marketplace/__init__.py:247-403) andremove()body (lines 572-605) into a new private modulesrc/apm_cli/commands/marketplace/_source_ops.pywith two functions:_do_add_source(repos, name, branch, host, verbose, invoked_as_legacy)and_do_remove_source(name, yes, verbose, invoked_as_legacy).Replace the
marketplace addandmarketplace removeClick bodies with calls to those helpers, passinginvoked_as_legacy=True.Create
src/apm_cli/commands/source.pydefining top-leveladdandremoveClick commands that call the same helpers withinvoked_as_legacy=False.Wire the new commands into
src/apm_cli/cli.pyimmediately afterinstall/uninstallso they render adjacently in--help.Deprecation tip is emitted only when
invoked_as_legacy=TrueAND the operation succeeds. Uses the existing_rich_infohelper with[i]symbol; routed to stderr via a small extension to_rich_echoaccepting afile=kwarg, OR a plainclick.echo(..., err=True)fallback if the helper extension is deferred.Help-text reorg (apm
--help): out of scope for this PR. Promotingadd/removeadjacent toinstall/uninstallin registration order is enough; full categorization is a separate issue once we have 3+ promoted verbs (precedent:MarketplaceGroup.format_commands).Files touched
src/apm_cli/commands/marketplace/_source_ops.pysrc/apm_cli/commands/marketplace/__init__.pyadd/removebodies become 1-line calls.src/apm_cli/commands/source.pysrc/apm_cli/cli.pyadd,removeafterinstall/uninstall.src/apm_cli/utils/console.py_rich_echowithfile=kwarg.Acceptance Criteria
apm add OWNER/REPOregisters a marketplace source (same outcome asapm marketplace add OWNER/REPO).apm add A/B C/Dregisters multiple sources; partial failure yields exit code 1 with a summary line.apm remove NAMEremoves a registered marketplace.apm marketplace addandapm marketplace removestill 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/derrors:--namerequires a single source.apm add --helpshowsOWNER/REPO [OWNER/REPO ...]usage.apm marketplace add --helpincludes "(alias: apm add)"..github/instructions/encoding.instructions.md).tests/unit/marketplace/test_marketplace_commands.pytests pass unchanged (logic extracted, behavior preserved).tests/unit/commands/test_source_commands.pycovers: 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/anduv run --extra dev ruff format --check src/ tests/pass clean.[Unreleased]->Addedentry.Docs Checklist (in same PR)
README.md-- swap quickstart marketplace example toapm add github/awesome-copilot; one-line note on long form.docs/src/content/docs/reference/cli-commands.md-- addapm addandapm removereference entries; annotateapm marketplace add/removeheadings with "(alias: apm add/remove)"; add rows to the package-commands table.docs/src/content/docs/guides/marketplaces.md-- swap primary register/remove examples toapm add/apm remove; one inline note explains alias relationship.docs/src/content/docs/guides/marketplace-authoring.md-- update the consumer-facing line toapm add <owner>/<repo>(long form once in parens).docs/src/content/docs/enterprise/registry-proxy.md-- update proxy compatibility table cell to includeapm add.packages/apm-guide/.apm/skills/apm-usage/commands.md-- add canonical rows forapm add OWNER/REPOandapm remove NAME; markapm marketplace add|removeas long form so the skill teachesapm addfirst.:::noteadmonitions under the newapm add/apm removereference headings clarifying the alias relationship.Out of Scope (file separately)
apm marketplace listto a top-levelapm list --sources(or similar). Its UX questions (filtering, output format) deserve their own discussion. The CEO lens explicitly recommended scoping this issue toadd+removeonly.MarketplaceGroup.format_commandspattern to root in a follow-up once 3+ verbs are promoted.apm install <name>when<name>resolves against an already-registered source. Tracked as supply-chain follow-up.Strategic Rationale
References
apm searchpromotion):src/apm_cli/cli.py:92apm info->apm view, hidden, no tip):src/apm_cli/cli.py:64-73Orchestration Provenance
This issue was synthesized via the genesis-skill orchestration pattern: parallel fan-out across
oss-growth-hacker+devx-ux-expert(research wave), fusedpython-architect+cli-logging-uxfor design,doc-writerfor docs delta, parallel sanity gate fromsupply-chain-security-expert+apm-ceo. Both gate verdicts incorporated in Acceptance Criteria.