Skip to content

fix(marketplace): add honors manifest.name for Claude Code parity#1032

Merged
danielmeppiel merged 1 commit intomainfrom
fix/marketplace-add-respects-manifest-name
Apr 29, 2026
Merged

fix(marketplace): add honors manifest.name for Claude Code parity#1032
danielmeppiel merged 1 commit intomainfrom
fix/marketplace-add-respects-manifest-name

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

TL;DR

apm marketplace add now defaults the local alias to the name field declared inside the fetched marketplace.json, falling back to the repo name only when the manifest omits it (or declares something invalid). This restores verbatim portability with Claude Code install instructions — addyosmani/agent-skills now registers as addy-agent-skills, exactly as that repo's README documents — and is a no-op for github/awesome-copilot (manifest name == repo name).

Problem (WHY)

The marketplace.json spec, originated by Claude Code, treats the manifest's top-level name as the canonical alias. APM was the only consumer that ignored it.

  • Reproducer. addyosmani/agent-skills ships .claude-plugin/marketplace.json with "name": "addy-agent-skills". Claude Code's /plugin install agent-skills@addy-agent-skills works; APM forced agent-skills@agent-skills (repo name). Addy's published install command was therefore broken under APM.
  • Code path. add in src/apm_cli/commands/marketplace.py computed display_name = name or repo_name before fetching the manifest, then fetched and discarded manifest.name.
  • Spec alignment. The Claude Code marketplace format documents name as a required, identifying field of the marketplace itself (source: marketplace.json reference).
  • Surface area. MarketplaceManifest already parses name from JSON; the data is there, just unused.
  • Blast radius. Every Claude-Code-authored marketplace whose name differs from its repo name produces a different alias under APM than under any other tool. Users hit this immediately, on the very next command (apm install <plugin>@<alias>).

Note

This only affects newly-added marketplaces. Existing entries in ~/.apm/marketplaces.json are untouched; users who already added a marketplace keep their alias.

Approach (WHAT)

Tier Source of alias Behaviour on invalid value
1 --name flag Hard-fail with the existing error
2 manifest.name (after fetch) Soft-fallback to tier 3 + yellow warning
3 Repo name (Always valid by GitHub naming rules)

Restructured add so that fetch_marketplace runs before the alias is finalized and before logger.start prints the headline. The probe source for _auto_detect_path keeps a placeholder name (it only consults host/owner/repo).

Implementation (HOW)

File Change
src/apm_cli/commands/marketplace.py New _is_valid_alias helper (single regex, single call site). add: hard-validate --name early; build probe source with placeholder name; fetch manifest; resolve display_name via 3-tier precedence; print headline + verbose Alias source: line; emit install hint only when alias differs from repo name and user did not pass --name.
tests/unit/marketplace/test_marketplace_commands.py 7 new tests covering each precedence tier, the awesome-copilot regression case, the verbose Alias source trace, and --name rejection.

Tip

The _is_valid_alias helper stays inlined in marketplace.py rather than promoted to utils/. Per the python-architect persona's "abstract when 3+ call sites" rule, single call site = no extraction.

Diagram

Resolution flow for display_name after the fix:

flowchart TD
  A["apm marketplace add OWNER/REPO [--name X]"] --> B{--name passed?}
  B -- yes --> C{_is_valid_alias?}
  C -- no --> Z1[hard-fail: invalid CLI name]
  C -- yes --> D[display_name = X]
  B -- no --> E[fetch_marketplace]
  E --> F{manifest.name set?}
  F -- no --> G[display_name = repo_name]
  F -- yes --> H{_is_valid_alias?}
  H -- yes --> I["display_name = manifest.name"]
  H -- no --> J["warn + display_name = repo_name"]
  D --> K[register + verbose 'Alias source' line]
  G --> K
  I --> K
  J --> K
  K --> L{alias != repo_name AND --name not used?}
  L -- yes --> M["info hint: apm install <plugin>@<alias>"]
  L -- no --> N[done]
Loading

Trade-offs

  • Soft-fail vs hard-fail on invalid manifest.name. Chose soft-fail because the user typed a valid OWNER/REPO; punishing them for a publisher mistake would block the install entirely. The yellow warning quotes the offending value so the publisher (often the same person reading) can fix it.
  • Behaviour change for unpinned scripts. Any automation that re-runs apm marketplace add X/Y and assumed the alias would be Y will pick up a different alias if Y's manifest declares one. Mitigation: --name is the explicit escape hatch; documented in the commit body.
  • Help text on --name left as (defaults to repo name). Strictly the default is now manifest.name or repo_name. Updating help text was deferred to keep the patch minimal and to avoid relitigating the option's docstring; the verbose Alias source: line surfaces the actual resolution.
  • Single extra MarketplaceSource construction (frozen dataclass, cheap) preferred over plumbing a mutable name through the probe path.

Validation

Live smoke test against the two real-world marketplaces — Addy's repo (different manifest name) and awesome-copilot (same manifest name as repo):

Default mode
$ apm marketplace add addyosmani/agent-skills
[*] Registering marketplace 'addy-agent-skills'...
[+] Marketplace 'addy-agent-skills' registered (1 plugins)
[i] Install plugins with: apm install <plugin>@addy-agent-skills

$ apm marketplace add github/awesome-copilot
[*] Registering marketplace 'awesome-copilot'...
[+] Marketplace 'awesome-copilot' registered (73 plugins)
Verbose mode (Addy's repo)
[*] Registering marketplace 'addy-agent-skills'...
    Repository: addyosmani/agent-skills
    Branch: main
    Detected path: .claude-plugin/marketplace.json
    Alias source: manifest.name ('addy-agent-skills')
[+] Marketplace 'addy-agent-skills' registered (1 plugins)
[i] Install plugins with: apm install <plugin>@addy-agent-skills
Browse confirms parity with Claude Code install syntax
$ apm marketplace browse addy-agent-skills
...
| agent-skills | ... | -- | agent-skills@addy-agent-skills |
[i] Install a plugin: apm install <plugin-name>@addy-agent-skills

Test suite — full marketplace surface, 0 regressions, 7 new tests:

$ pytest tests/unit/marketplace/ tests/unit/commands/test_marketplace_plugin.py \
         tests/integration/test_marketplace_e2e.py -q
827 passed, 1 skipped in 10.35s

How to test

  • apm marketplace add addyosmani/agent-skills — registers as addy-agent-skills; hint shows the new alias.
  • apm marketplace add github/awesome-copilot — registers as awesome-copilot; no hint (alias == repo name).
  • apm marketplace add OWNER/REPO --name foo — registers as foo; no hint regardless of manifest.
  • apm marketplace add OWNER/REPO --name "bad name" — hard-fails with the existing Invalid marketplace name error.
  • apm marketplace add addyosmani/agent-skills -v — verbose output includes Alias source: manifest.name ('addy-agent-skills').
  • After (1), apm install agent-skills@addy-agent-skills succeeds — verbatim copy of Addy's README install command.

Previously, `apm marketplace add OWNER/REPO` always defaulted the
local marketplace alias to the repo name, ignoring the `name` field
declared inside the fetched marketplace.json. This broke verbatim
portability of Claude Code install instructions: a publisher whose
manifest declares "name": "addy-agent-skills" expects users to run
`apm install <plugin>@addy-agent-skills`, but APM forced
`@agent-skills` (the repo name) instead.

New precedence: --name flag > manifest.name (if valid) > repo name.
Invalid manifest.name triggers a soft fallback with a warning so a
publisher mistake never breaks a successful add. Aliases that
differ from the repo name surface a one-line install hint pointing
to the new alias.

For github/awesome-copilot (manifest.name == "awesome-copilot" ==
repo) the behaviour is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 10:51
@danielmeppiel danielmeppiel merged commit 3c39504 into main Apr 29, 2026
17 checks passed
@danielmeppiel danielmeppiel deleted the fix/marketplace-add-respects-manifest-name branch April 29, 2026 10:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates apm marketplace add to derive the registered marketplace alias from marketplace.json's top-level name (when valid), aligning APM behavior with Claude Code marketplace expectations.

Changes:

  • Add alias validation helper and update marketplace add alias resolution precedence: --name > valid manifest.name > repo name (with a soft-fallback warning for invalid manifest names).
  • Reorder add flow so the manifest is fetched before printing the “Registering marketplace ...” headline, and add a verbose “Alias source” trace line.
  • Add unit tests covering the new precedence rules, warnings, and verbose output.
Show a summary per file
File Description
src/apm_cli/commands/marketplace.py Implements manifest-driven alias resolution + validation and adjusts logging/hints accordingly.
tests/unit/marketplace/test_marketplace_commands.py Adds tests for the new alias precedence behavior and output expectations.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 5

Comment on lines +418 to +420
assert _is_valid_alias(display_name), (
f"Resolved marketplace alias '{display_name}' failed validation"
)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid relying on assert for runtime invariants in CLI code: asserts are skipped under python -O, so this defense-in-depth check may silently disappear. Prefer an explicit conditional that logs an error and exits (or raises) if the invariant is violated.

Suggested change
assert _is_valid_alias(display_name), (
f"Resolved marketplace alias '{display_name}' failed validation"
)
if not _is_valid_alias(display_name):
raise click.ClickException(
f"Resolved marketplace alias '{display_name}' failed validation"
)

Copilot uses AI. Check for mistakes.
Comment on lines +397 to +398
manifest_name = (manifest.name or "").strip()
if name is not None:
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifest.name comes from external JSON and is not guaranteed to be a string (e.g., name: 123); calling .strip() on a non-str will raise AttributeError and break apm marketplace add. Consider normalizing defensively (only .strip() when isinstance(..., str)) and treating non-string values as "missing/invalid" so you can fall back cleanly.

Copilot uses AI. Check for mistakes.
Comment on lines +407 to +412
logger.warning(
f"Manifest declares name '{manifest_name}' which is not a "
f"valid alias (must match [a-zA-Z0-9._-]+). "
f"Falling back to repo name."
)
alias_source = f"repo name (manifest.name '{manifest_name}' invalid)"
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning/verbose output interpolates manifest_name directly. If the manifest contains non-ASCII or control characters, this can violate the project's ASCII-only CLI-output rule and can also produce messy/unsafe terminal output. Please sanitize/escape the displayed value (e.g., backslash-escape non-printables) before logging it.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +381 to +392
# Fetch and validate the manifest before logging start, so that the
# success/start lines display the *final* alias the user must use.
fetch_source = MarketplaceSource(
name=name or repo_name,
owner=owner,
repo=repo_name,
branch=branch,
host=resolved_host,
path=detected_path,
)
manifest = fetch_marketplace(fetch_source, force_refresh=True)
plugin_count = len(manifest.plugins)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch_marketplace() caches by source.name (see marketplace/client.py::_cache_key). When the manifest alias differs from the repo name, this add path fetches/caches under the repo name, then registers under the manifest alias, so the next install/browse/search by alias will miss cache and re-fetch immediately. Consider writing the fetched data to the cache key for the final alias (or changing the cache key to be based on host/owner/repo/path rather than alias) to avoid the extra network call.

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +401
# Resolve final alias: --name flag > manifest.name (if valid) > repo name.
# Track which tier won so we can report it in verbose mode and emit a
# warning when a publisher-declared name had to be rejected.
manifest_name = (manifest.name or "").strip()
if name is not None:
display_name = name
alias_source = "--name flag"
elif manifest_name and _is_valid_alias(manifest_name):
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI/docs now describe --name as "defaults to repo name", but add now effectively defaults to manifest.name when present. Please update the --name option help string and the relevant docs pages (e.g., docs/src/content/docs/guides/marketplaces.md, docs/src/content/docs/reference/cli-commands.md) plus packages/apm-guide/.apm/skills/apm-usage/commands.md to match the new precedence rules.

Copilot generated this review using guidance from repository custom instructions.
danielmeppiel pushed a commit that referenced this pull request Apr 29, 2026
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps
pyproject.toml + uv.lock to 0.11.0.

Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because
this release ships one BREAKING removal (`apm marketplace build` -> exits 2,
use `apm pack`) plus several net-new features (Dev Container Feature,
Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack`
unification, multi-org `apps[]`). Strict semver in 0.x: minor for
features-with-break, patch only for bugfixes.

Milestone admin (done out-of-band):
- Renamed milestone #8 `0.10.1` -> `0.11.0`
- Created milestone #9 `0.12.0` as next-up bucket
- Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0`
- 6 closed items stay in `0.11.0`

PRs shipping in 0.11.0 (22 commits since v0.10.0):

User-facing features:
- #1042/#722 `apm pack` unifies bundle + marketplace.json
                   (BREAKING: `apm marketplace build` removed)
- #1038       `marketplace:` block in apm.yml + `apm marketplace migrate`
- #803  /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives
- #861        Dev Container Feature `ghcr.io/microsoft/apm/apm-cli`
- #982/#984   shared/apm.md `apps:` array for cross-org private packages
- #820        `target:` in apm.yml validates at parse time
- #1032       `apm marketplace add` honors manifest.name (Claude Code parity)
- #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms

User-facing fixes:
- #1015 ADO Entra ID auth + `apm install --update` pre-flight abort
- #1019/#1020 GEMINI.md only created when target requested
- #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms
- #1018 POSIX paths in auto-discovery output (Windows compat)
- #996  drop stray 'specify' from generated file footer

Maintainer tooling:
- #1043 NOTICE.md per CELA template
- #1045/#1044 NOTICE drift gate + license-policy gate in CI
- #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut)
- #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1
- #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file
- #1022 review-panel: true fan-out + binary verdict + label automation
- #918  complexity audit + benchmarks suite
- #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder)

Files changed:
- pyproject.toml: 0.10.0 -> 0.11.0
- uv.lock:        regenerated (version field only)
- CHANGELOG.md:   [Unreleased] promoted to [0.11.0] - 2026-04-29

NOTICE drift check passes against the bumped lockfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added this to the 0.11.0 milestone Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants