fix(marketplace): add honors manifest.name for Claude Code parity#1032
fix(marketplace): add honors manifest.name for Claude Code parity#1032danielmeppiel merged 1 commit intomainfrom
add honors manifest.name for Claude Code parity#1032Conversation
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>
There was a problem hiding this comment.
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 addalias resolution precedence:--name> validmanifest.name> repo name (with a soft-fallback warning for invalid manifest names). - Reorder
addflow 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
| assert _is_valid_alias(display_name), ( | ||
| f"Resolved marketplace alias '{display_name}' failed validation" | ||
| ) |
There was a problem hiding this comment.
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.
| 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" | |
| ) |
| manifest_name = (manifest.name or "").strip() | ||
| if name is not None: |
There was a problem hiding this comment.
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.
| 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)" |
There was a problem hiding this comment.
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.
| # 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) |
There was a problem hiding this comment.
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.
| # 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): |
There was a problem hiding this comment.
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.
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>
TL;DR
apm marketplace addnow defaults the local alias to thenamefield declared inside the fetchedmarketplace.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-skillsnow registers asaddy-agent-skills, exactly as that repo's README documents — and is a no-op forgithub/awesome-copilot(manifest name == repo name).Problem (WHY)
The marketplace.json spec, originated by Claude Code, treats the manifest's top-level
nameas the canonical alias. APM was the only consumer that ignored it.addyosmani/agent-skillsships.claude-plugin/marketplace.jsonwith"name": "addy-agent-skills". Claude Code's/plugin install agent-skills@addy-agent-skillsworks; APM forcedagent-skills@agent-skills(repo name). Addy's published install command was therefore broken under APM.addinsrc/apm_cli/commands/marketplace.pycomputeddisplay_name = name or repo_namebefore fetching the manifest, then fetched and discardedmanifest.name.nameas a required, identifying field of the marketplace itself (source: marketplace.json reference).MarketplaceManifestalready parsesnamefrom JSON; the data is there, just unused.namediffers 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.jsonare untouched; users who already added a marketplace keep their alias.Approach (WHAT)
--nameflagmanifest.name(after fetch)Restructured
addso thatfetch_marketplaceruns before the alias is finalized and beforelogger.startprints the headline. The probe source for_auto_detect_pathkeeps a placeholder name (it only consults host/owner/repo).Implementation (HOW)
src/apm_cli/commands/marketplace.py_is_valid_aliashelper (single regex, single call site).add: hard-validate--nameearly; build probe source with placeholder name; fetch manifest; resolvedisplay_namevia 3-tier precedence; print headline + verboseAlias source:line; emit install hint only when alias differs from repo name and user did not pass--name.tests/unit/marketplace/test_marketplace_commands.pyAlias sourcetrace, and--namerejection.Tip
The
_is_valid_aliashelper stays inlined inmarketplace.pyrather than promoted toutils/. Per the python-architect persona's "abstract when 3+ call sites" rule, single call site = no extraction.Diagram
Resolution flow for
display_nameafter 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]Trade-offs
manifest.name. Chose soft-fail because the user typed a validOWNER/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.apm marketplace add X/Yand assumed the alias would beYwill pick up a different alias ifY's manifest declares one. Mitigation:--nameis the explicit escape hatch; documented in the commit body.--nameleft as(defaults to repo name). Strictly the default is nowmanifest.name or repo_name. Updating help text was deferred to keep the patch minimal and to avoid relitigating the option's docstring; the verboseAlias source:line surfaces the actual resolution.MarketplaceSourceconstruction (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
Verbose mode (Addy's repo)
Browse confirms parity with Claude Code install syntax
Test suite — full marketplace surface, 0 regressions, 7 new tests:
How to test
apm marketplace add addyosmani/agent-skills— registers asaddy-agent-skills; hint shows the new alias.apm marketplace add github/awesome-copilot— registers asawesome-copilot; no hint (alias == repo name).apm marketplace add OWNER/REPO --name foo— registers asfoo; no hint regardless of manifest.apm marketplace add OWNER/REPO --name "bad name"— hard-fails with the existingInvalid marketplace nameerror.apm marketplace add addyosmani/agent-skills -v— verbose output includesAlias source: manifest.name ('addy-agent-skills').apm install agent-skills@addy-agent-skillssucceeds — verbatim copy of Addy's README install command.