Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions docs/src/content/docs/producer/publish-to-a-marketplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description: Author an APM marketplace registry, build it, and publish updates t

A **marketplace** in APM is a curated index of packages that one repo
publishes and many repos install from. You author it as a
`marketplace:` block in `apm.yml`, build it into
`.claude-plugin/marketplace.json` with `apm pack`, and let consumers
register your repo with `apm marketplace add`. This page walks the
`marketplace:` block in `apm.yml`, build it into one or more
marketplace artifacts with `apm pack`, and let consumers register
your repo with `apm marketplace add`. This page walks the
producer side: schema, build, and the optional `apm marketplace
publish` flow that opens PRs against pinned consumer repos.

Expand All @@ -20,8 +20,8 @@ packages from it -- see [Marketplaces](../../guides/marketplaces/).
apm marketplace init # 1. add the block to apm.yml
$EDITOR apm.yml # 2. describe each package
apm marketplace check # 3. validate refs resolve
apm pack # 4. compile marketplace.json
git add apm.yml .claude-plugin/marketplace.json
apm pack # 4. compile marketplace artifacts
git add apm.yml .claude-plugin/marketplace.json .agents/plugins/marketplace.json
git commit -m "Release v1.0.0" && git push # 5. ship
```

Expand All @@ -40,12 +40,15 @@ APM uses a single source-of-truth model:

- `apm.yml` -- hand-edited. The `marketplace:` block declares your
registry: owner, packages, version ranges.
- `.claude-plugin/marketplace.json` -- generated by `apm pack`.
- `.claude-plugin/marketplace.json` -- generated by `apm pack` by default.
Byte-compatible with [Anthropic's marketplace.json](https://docs.claude.com/en/docs/claude-code/plugin-marketplaces)
so Claude Code, Copilot CLI, and APM all read the same artefact.
- `.agents/plugins/marketplace.json` -- optional Codex repo marketplace
output. Enable it by adding `codex` to `marketplace.outputs`.

Both files are committed. The legacy standalone `marketplace.yml` is
deprecated; if you still have one, run `apm marketplace migrate`.
Commit every generated file matching your enabled `marketplace.outputs`.
The legacy standalone `marketplace.yml` is deprecated; if you still
have one, run `apm marketplace migrate`.

## Author the registry

Expand All @@ -68,6 +71,14 @@ marketplace:
name: acme-org
url: https://github.com/acme-org

outputs: [claude] # default; add codex for Codex repo output

claude:
output: .claude-plugin/marketplace.json

codex:
output: .agents/plugins/marketplace.json

build:
tagPattern: "v{version}" # default; per-package overridable

Expand All @@ -84,6 +95,7 @@ marketplace:
- name: local-tool
source: ./packages/local-tool # ships from this same repo
version: 0.1.0
category: Productivity # required when outputs includes codex
```

The key in `apm.yml` is `packages:`. It becomes `plugins:` in the
Expand All @@ -93,8 +105,28 @@ error, never silently ignored.

For the full field reference (every key on every entry, including
`subdir`, `tag_pattern`, `include_prerelease`, `metadata`,
`pluginRoot`, and Anthropic pass-through fields like `tags`, `author`,
`license`), see [Marketplace authoring](../../guides/marketplace-authoring/).
`pluginRoot`, `outputs`, `claude`, `codex`, and pass-through fields
like `tags`, `author`, `license`), see the reference below.

Marketplace output targets use a selector-list pattern:

```yaml
marketplace:
outputs: [claude, codex]
```

Claude output is selected by default for backwards compatibility. The
legacy `marketplace.output` field remains supported as shorthand for
`marketplace.claude.output`; when both are set, the explicit
`claude.output` value wins. Configure output paths in `apm.yml`:
`marketplace.claude.output` controls the Claude/Anthropic artifact,
and `marketplace.codex.output` controls the Codex artifact. The legacy
`--marketplace-output` flag remains for compatibility, but overrides
only the Claude/Anthropic output path.

When `codex` is selected, every package must define `category`. Codex
output maps local entries to `source: local`, remote entries to
`source: url`, and remote subdirectory entries to `source: git-subdir`.

## Build

Expand All @@ -103,8 +135,8 @@ apm pack
```

`apm pack` resolves every remote `packages:` entry against
`git ls-remote`, leaves local-path entries untouched, and writes
`.claude-plugin/marketplace.json` atomically. Useful flags:
`git ls-remote`, leaves local-path entries untouched, and writes each
selected marketplace output atomically. Useful flags:

```bash
apm pack --dry-run # resolve and print; do not write
Expand Down Expand Up @@ -173,9 +205,11 @@ consumers running `apm install --update` on their own cadence.
- **Both `apm.yml` (`marketplace:` block) and `marketplace.yml`
present** is a hard error. Pick one; prefer the block and run
`apm marketplace migrate` to consolidate.
- **`*.json` in `.gitignore`** will silently skip the generated file.
- **`*.json` in `.gitignore`** will silently skip generated files.
`apm marketplace init` warns on this; if you hit it, add an
unignore: `!.claude-plugin/marketplace.json`.
unignore for every enabled output, such as
`!.claude-plugin/marketplace.json` and
`!.agents/plugins/marketplace.json`.
- **Local-path entries skip git resolution.** They emit the path
verbatim; consumers see the same path. Use `metadata.pluginRoot` if
your plugins live under a common subdirectory.
Expand Down
26 changes: 17 additions & 9 deletions docs/src/content/docs/reference/cli/pack.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: apm pack
description: Pack distributable artifacts (plugin bundle, APM bundle, or marketplace.json) from your APM project.
description: Pack distributable artifacts (plugin bundle, APM bundle, or marketplace artifacts) from your APM project.
sidebar:
order: 17
---
Expand All @@ -16,8 +16,8 @@ apm pack [OPTIONS]
`apm pack` produces distributable artifacts from the current APM project. It reads `apm.yml` to decide what to emit:

- `dependencies:` block present -> a bundle (directory or `.tar.gz`).
- `marketplace:` block present -> `.claude-plugin/marketplace.json`.
- Both blocks present -> both artifacts in a single run.
- `marketplace:` block present -> selected marketplace artifacts.
- Both blocks present -> bundle plus selected marketplace artifacts in a single run.

The bundle is built from `apm.lock.yaml`. An enriched copy of the lockfile (per-file SHA-256 in `bundle_files`, plus `pack:` metadata) is embedded inside the bundle so `apm install <bundle>` can verify integrity at install time.

Expand All @@ -35,7 +35,7 @@ Bundles are target-agnostic. The consumer's project decides where files land at
| `--verbose`, `-v` | off | Show per-file paths and detailed packer output. |
| `--offline` | off | Marketplace: resolve version ranges from cached refs only; skip `git ls-remote`. |
| `--include-prerelease` | off | Marketplace: allow pre-release tags to satisfy version ranges. |
| `--marketplace-output PATH` | `.claude-plugin/marketplace.json` | Marketplace: override the output path. |
| `--marketplace-output PATH` | `.claude-plugin/marketplace.json` | Marketplace legacy compatibility: override only the Claude/Anthropic output path. Prefer `marketplace.claude.output` in `apm.yml`. |
| `--legacy-skill-paths` | off | Bundle skills under per-client paths (e.g. `.cursor/skills/`) instead of the converged `.agents/skills/`. Compatibility flag. |
| `--target`, `-t VALUE` | auto-detect | **Deprecated.** Recorded as informational `pack.target` metadata only; ignored by `apm install`. Will be removed in a future release. |

Expand Down Expand Up @@ -63,10 +63,15 @@ apm pack
apm pack --archive --offline
```

### Override marketplace output path
### Configure marketplace output paths

```bash
apm pack --marketplace-output ./build/marketplace.json
```yaml
marketplace:
outputs: [claude, codex]
claude:
output: ./build/claude-marketplace.json
codex:
output: ./build/codex-marketplace.json
```

### Preview without writing
Expand Down Expand Up @@ -106,9 +111,11 @@ dependencies:
- repo_url: owner/repo
```

### Marketplace artifact
### Marketplace artifacts

`.claude-plugin/marketplace.json` by default, plus any additional artifact selected by `marketplace.outputs` such as `.agents/plugins/marketplace.json` for Codex. Each remote plugin's version range is resolved against `git ls-remote`; local-path entries pass through verbatim. Files are written atomically, and parent directories are created if absent.

`.claude-plugin/marketplace.json` (or `--marketplace-output PATH`). Each remote plugin's version range is resolved against `git ls-remote`; local-path entries pass through verbatim. The file is written atomically. `.claude-plugin/` is created if absent; nothing else is scaffolded there.
Configure marketplace artifact paths in `apm.yml`: `marketplace.claude.output` controls the Claude/Anthropic artifact, and `marketplace.codex.output` controls the Codex artifact. `--marketplace-output PATH` remains as a legacy Claude-only compatibility override; prefer manifest config for new projects and CI.

## Behavior

Expand All @@ -117,6 +124,7 @@ dependencies:
- **Empty bundle warning.** If no files match (e.g. nothing was installed yet), `apm pack` emits a warning and exits `0` with an empty bundle. Verbose mode prints a hint to run `apm install` first.
- **Share line.** On success, `apm pack` prints `Share with: apm install <bundle-path>` so the produced bundle is immediately copy-pasteable.
- **Marketplace fallback.** With no `marketplace:` block in `apm.yml`, a legacy `marketplace.yml` file is read with a deprecation warning. Both files present is a hard error.
- **Marketplace outputs.** `marketplace.outputs` defaults to `[claude]`. Add `codex` to also write `.agents/plugins/marketplace.json`; when selected, each package must define `category`.

## Exit codes

Expand Down
16 changes: 12 additions & 4 deletions src/apm_cli/commands/marketplace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def marketplace(ctx):


def _check_gitignore_for_marketplace_json(logger):
"""Warn if .gitignore contains a rule that would ignore marketplace.json."""
"""Warn if .gitignore contains a rule that would ignore marketplace outputs."""
gitignore_path = Path.cwd() / ".gitignore"
if not gitignore_path.exists():
return
Expand All @@ -221,7 +221,14 @@ def _check_gitignore_for_marketplace_json(logger):
except OSError:
return

patterns = {"marketplace.json", "**/marketplace.json", "/marketplace.json", "*.json"}
patterns = {
"marketplace.json",
"**/marketplace.json",
"/marketplace.json",
".claude-plugin/marketplace.json",
".agents/plugins/marketplace.json",
"*.json",
}
for line in lines:
stripped = line.strip()
# Skip blank and commented lines
Expand All @@ -230,8 +237,9 @@ def _check_gitignore_for_marketplace_json(logger):
if stripped in patterns:
logger.warning(
"Your .gitignore ignores marketplace.json. "
"Both apm.yml and the generated marketplace.json must be "
"tracked in git. Remove the .gitignore rule.",
"Track apm.yml plus generated marketplace files such as "
".claude-plugin/marketplace.json and .agents/plugins/marketplace.json. "
"Remove the .gitignore rule or add explicit unignore entries.",
symbol="warning",
)
return
Expand Down
3 changes: 2 additions & 1 deletion src/apm_cli/commands/marketplace/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def init(force, no_gitignore_check, name, owner, verbose):
next_steps = [
"Edit the 'marketplace:' block in apm.yml to add your packages",
"Run 'apm pack' to generate .claude-plugin/marketplace.json",
"Commit BOTH apm.yml and the generated marketplace.json",
"Add 'codex' to marketplace.outputs to also generate .agents/plugins/marketplace.json",
"Commit apm.yml and the generated marketplace file(s)",
]

try:
Expand Down
56 changes: 38 additions & 18 deletions src/apm_cli/commands/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
Reads apm.yml to decide what to produce:

dependencies: block -> bundle (directory or .tar.gz)
marketplace: block -> .claude-plugin/marketplace.json
both blocks present -> both artifacts
marketplace: block -> selected marketplace artifacts
both blocks present -> bundle plus selected marketplace artifacts

The lockfile (apm.lock.yaml) pins bundle contents. An enriched copy
is embedded in each bundle.
Expand All @@ -42,8 +42,8 @@
apm pack
apm pack --archive --offline

# Override marketplace.json location:
apm pack --marketplace-output ./build/marketplace.json
# Marketplace output paths are normally configured in apm.yml:
# marketplace.claude.output / marketplace.codex.output

Exit codes:
0 Success
Expand Down Expand Up @@ -104,7 +104,10 @@
"marketplace_output",
type=click.Path(),
default=None,
help="Marketplace: override output path (default: .claude-plugin/marketplace.json).",
help=(
"Marketplace legacy compatibility: override only the Claude/Anthropic "
"output path. Prefer marketplace.claude.output in apm.yml."
),
)
@click.option(
"--legacy-skill-paths",
Expand Down Expand Up @@ -180,7 +183,7 @@ def pack_cmd(
if sub.kind is OutputKind.BUNDLE:
_render_bundle_result(logger, sub.payload, fmt, target, dry_run)
elif sub.kind is OutputKind.MARKETPLACE:
_render_marketplace_result(logger, sub.payload, dry_run, sub.warnings)
_render_marketplace_result(logger, sub.payload, dry_run, sub.warnings, sub.outputs)


def _render_bundle_result(logger, pack_result, fmt, target, dry_run):
Expand Down Expand Up @@ -232,23 +235,40 @@ def _render_bundle_result(logger, pack_result, fmt, target, dry_run):
logger.info(f"Share with: apm install {pack_result.bundle_path}")


def _render_marketplace_result(logger, report, dry_run, extra_warnings=None):
def _render_marketplace_result(logger, report, dry_run, extra_warnings=None, outputs=None):
"""Render the marketplace producer's report (one-liner summary)."""
if report is None:
return
seen_warnings = set()
for warn_msg in extra_warnings or []:
seen_warnings.add(warn_msg)
logger.warning(warn_msg)
for warn_msg in report.warnings:
for warn_msg in getattr(report, "warnings", ()) or ():
if warn_msg in seen_warnings:
continue
seen_warnings.add(warn_msg)
logger.warning(warn_msg)
if dry_run or report.dry_run:
logger.dry_run_notice(
f"Would write marketplace.json ({len(report.resolved)} package(s)) "
f"-> {report.output_path}"
)

output_reports = tuple(getattr(report, "outputs", ()) or ())
if not output_reports:
package_count = len(getattr(report, "resolved", ()) or ()) if report is not None else None
for output in outputs or []:
message = f"marketplace.json -> {output}"
if package_count is not None:
message = f"marketplace.json ({package_count} package(s)) -> {output}"
if dry_run:
logger.dry_run_notice(f"Would write {message}")
else:
logger.success(f"Built {message}")
return
logger.success(
f"Built marketplace.json ({len(report.resolved)} package(s)) -> {report.output_path}"
)

for output_report in output_reports:
message = (
f"marketplace.json [{output_report.profile}] "
f"({len(output_report.resolved)} package(s)) -> {output_report.output_path}"
)
if dry_run or output_report.dry_run:
logger.dry_run_notice(f"Would write {message}")
else:
logger.success(f"Built {message}")


@click.command(
Expand Down
Loading
Loading