Skip to content

Marketplace output UX: filter-driven CLI + map-based manifest + JSON output #1317

@danielmeppiel

Description

@danielmeppiel

Marketplace output UX: filter-driven CLI + map-based manifest + JSON output

TL;DR

Refactor the apm pack marketplace surface so the manifest declares the universe and the CLI filters within it. Replace the asymmetric --marketplace-output (silently Claude-only) with format-symmetric flags that scale to the 3rd, 4th, Nth marketplace format with zero CLI churn.

Origin: follow-up to #1281 (review thread). Today's PR is sound and should ship; this issue tracks the surface that makes "write once, publish to every AI assistant" frictionless in CI/CD.

Problem

Today (post #1281):

marketplace.outputs: [claude, codex]      # list of names
marketplace.claude.output: PATH           # sibling block, per-format
marketplace.codex.output:  PATH           # sibling block, per-format
--marketplace-output PATH                  # CLI: silently claude-only

Three concrete UX bugs:

  1. Asymmetric surface. apm pack --marketplace-output X reads as "set the marketplace path" but only touches Claude. CI scripts adopting outputs:[claude,codex] will silently drop the Codex artifact override, with no warning.
  2. No CLI filter. No way to say "this CI matrix job builds claude only" without editing apm.yml. Matrix builds devolve into shell hacks or per-job manifest copies.
  3. Magic-string registry. Adding cursor / windsurf / gemini later means another round of CLI-flag bikeshedding (--cursor-output? --marketplace-cursor? rename the existing flag again?).
flowchart LR
    subgraph today["TODAY (PR #1281)"]
        T1["apm.yml<br/>marketplace.outputs: [claude, codex]<br/>marketplace.claude.output: ...<br/>marketplace.codex.output: ..."]
        T2["apm pack<br/>--marketplace-output PATH<br/>(silently claude-only)"]
        T1 --> T2
    end

    subgraph target["PROPOSED"]
        P1["apm.yml<br/>marketplace.outputs:<br/>  claude: {}<br/>  codex: {path: ...}"]
        P2["apm pack<br/>--marketplace=claude,codex<br/>--output-path codex=PATH<br/>--json"]
        P1 --> P2
    end

    today -.->|"v0.14 migration"| target
Loading

Proposed UX

Principle: the manifest declares the universe. The CLI filters within it.

This is the convergent pattern across docker buildx --platform, goreleaser --id, webpack --config-name, cargo --target. APM's prior art (hugo outputs:) goes config-only; we go one better by exposing a clean CLI filter for CI matrices.

1. Manifest — outputs is a map

marketplace:
  outputs:                                 # MAP, not list
    claude: {}                             # path defaults to .claude-plugin/marketplace.json
    codex:
      path: .agents/plugins/marketplace.json
  packages:
    - name: foo
      source: ./plugins/foo
      category: Productivity               # required iff outputs.codex is configured
  • One block per format. Per-format config (path, format-specific required fields) lives inside the entry. Eliminates the dual-source confusion (outputs: [claude, codex] selector + separate claude: + codex: sibling blocks).
  • New formats add new keys; nothing else moves.
  • Migration: outputs: [claude, codex] (list form) keeps parsing as shorthand for {claude: {}, codex: {}}; emits a one-cycle deprecation warning.
  • apm marketplace init writes outputs: {claude: {}} explicitly so the next-step ("add codex") is discoverable in the file itself, not buried in docs.

2. CLI — filter, don't path-mutate

apm pack                                   # build every format in outputs:
apm pack --marketplace=claude              # filter to one
apm pack --marketplace=claude,codex        # comma list
apm pack --marketplace claude --marketplace codex   # repeatable (same as above)
apm pack --marketplace=all                 # explicit "every configured"
apm pack --marketplace=none                # skip marketplace entirely; bundle-only

Mental model: selection, never silent path mutation. Same idiom every developer who's used Docker, goreleaser, or cargo already knows.

3. Path overrides — namespaced, format-aware, CI-targeted

apm pack --output-path claude=./build/claude.json
apm pack --output-path claude=./build/claude.json --output-path codex=./dist/codex.json

Or via env (12-factor / secrets-friendly):

APM_MARKETPLACE_CLAUDE_PATH=./build/x.json apm pack

Format-named on both sides → impossible to silently narrow scope. Every future format gets path overrides for free with zero CLI churn.

4. Machine-readable output for CI

apm pack --marketplace=all --json
{
  "ok": true,
  "outputs": [
    {"format": "claude", "path": ".claude-plugin/marketplace.json", "packages": 12, "added": 1, "updated": 2, "unchanged": 9},
    {"format": "codex",  "path": ".agents/plugins/marketplace.json", "packages": 12, "added": 1, "updated": 2, "unchanged": 9}
  ],
  "warnings": []
}

This is the killer CI featurejq -r '.outputs[].path' and pipe to actions/upload-artifact or gh release upload. Zero hardcoded paths in workflows.

5. Real deprecation for --marketplace-output

When the legacy flag is passed:

[!] --marketplace-output is deprecated; use --output-path claude=PATH or
    set marketplace.outputs.claude.path in apm.yml. Will be removed in v0.15.

Behavior auto-translates to --output-path claude=X for one minor cycle, then removed. Tracker: see linked removal issue.

6. Failure mode is the product

$ apm pack --marketplace=cursor
[x] Unknown marketplace format 'cursor'.
    Configured in apm.yml: claude, codex.
    Supported by APM:      claude, codex.

$ apm pack --marketplace=codex   # when yml has only {claude: {}}
[x] Marketplace format 'codex' is not configured in apm.yml.
    Add it under marketplace.outputs.codex, or run with --marketplace=claude.

Names what failed, why, one concrete next action. No tracebacks.

Behavior flow

flowchart TD
    A["apm.yml<br/>marketplace.outputs map"] --> B["MarketplaceConfig"]
    B --> C{"--marketplace filter?"}
    C -->|"unset / =all"| D["every configured format"]
    C -->|"=claude,codex"| E["filter to selected"]
    C -->|"=none"| F["skip marketplace<br/>(bundle only)"]
    D --> G["per-format build loop"]
    E --> G
    G --> H["MarketplaceOutputProfile<br/>registry lookup"]
    H --> I["Mapper.compose"]
    I --> J{"--output-path<br/>FORMAT=PATH?"}
    J -->|"yes"| K["use override"]
    J -->|"no"| L["use outputs.FORMAT.path<br/>or default"]
    K --> M["atomic write"]
    L --> M
    M --> N{"--json?"}
    N -->|"yes"| O["stdout JSON<br/>{outputs: [{format, path, ...}]}"]
    N -->|"no"| P["[claude] Built ...<br/>[codex]  Built ..."]
Loading

Adversarial review — build-tool peers

Reviewer Verdict Note
docker buildx --platform ✅ Direct match Comma-separated multi-value filter on a single canonical flag is exactly the buildx idiom. Per-output progress prefix [claude] / [codex] already in #1281.
goreleaser --id ✅ Direct match Filter-by-name within a config-defined registry. APM adds --json output, which goreleaser users routinely wish for.
webpack --config-name ✅ Match with one ask Webpack supports --config-name='!claude' exclusion. Resolution: not v1; YAGNI. Comma list covers 95%; ship without exclusion until first user asks.
cargo --target ⚠️ Partial Cargo's --target is single-value, repeated via shell loops. Resolution: APM accepts both comma-separated AND repeatable --marketplace foo --marketplace bar. Click multiple=True handles it.
rollup config-driven outputs ✅ Match Rollup deprecated CLI --format in favor of config arrays — exactly the direction we're going.
hugo outputs: ✅ APM is better Hugo never lets you filter at CLI — config only. Our --marketplace= is a strict superset for the CI matrix use case.
vite multi-config files ✅ APM is more disciplined Vite encourages multiple config files; APM keeps one manifest. Cleaner mental model.
npm publish --tag ⚠️ Default-state caution NPM was burned by changing default publish behavior. Resolution: bare apm pack builds whatever outputs: declares — never magical "all formats supported by APM." init writes the explicit {claude: {}} so the default is observable in the file.
terraform -target ✅ Lower-risk variant Terraform's -target is dangerous (selective apply); ours is read-only-ish (selective build). Same UX, less footgun.

Convergent verdict: ship the filter-flag + namespaced-path-override + JSON-output trio. The two real concerns (default-state surprise, exclusion syntax) both resolve by not doing the speculative thing.

CI/CD beauty — the real test

Matrix publish (parallel jobs, one format each):

strategy:
  matrix:
    format: [claude, codex]
steps:
  - run: apm pack --marketplace=${{ matrix.format }} --json > pack.json
  - run: gh release upload "v${VERSION}" $(jq -r '.outputs[0].path' pack.json)

Single job, all formats, fan-out upload:

- run: apm pack --marketplace=all --json | tee pack.json
- run: |
    jq -r '.outputs[] | "\(.format) \(.path)"' pack.json | while read fmt path; do
      gh release upload "v${VERSION}" "$path" --label "marketplace-$fmt"
    done

Local dev (fast iteration on one format):

apm pack --marketplace=claude --dry-run    # preview claude
apm pack --marketplace=codex               # write only codex
apm pack --marketplace=none                # bundle only

Day-one when cursor ships — zero CLI changes:

marketplace:
  outputs:
    claude: {}
    codex: {}
    cursor:
      path: .cursor/marketplace.json
apm pack --marketplace=cursor --json       # works immediately
sequenceDiagram
    participant CI as GitHub Actions
    participant APM as apm pack
    participant FS as Workspace
    participant GH as gh release upload

    Note over CI: matrix.format = [claude, codex]
    CI->>APM: apm pack --marketplace=${matrix.format} --json
    APM->>FS: write per-format artifact
    APM-->>CI: stdout: {outputs:[{format, path, ...}]}
    CI->>CI: jq -r '.outputs[0].path' pack.json
    CI->>GH: upload $(path) labelled marketplace-${matrix.format}
    Note over GH: One release, one artifact per format,<br/>zero hardcoded paths in workflow
Loading

Why this matters strategically

This is the plugin point the OSS-growth-hacker persona called "untapped contributor funnel" in the #1281 review. Today, adding a marketplace format is a CLI-design discussion. Under this proposal, it's a one-file PR (output_profiles.py + a mapper). The CLI surface is invariant under format growth.

It also gives APM the strongest version of its "write once, publish to every AI assistant" positioning:

  • The manifest is one block, observable in apm.yml review.
  • The CLI is one flag, identical across formats.
  • The CI artifact path is machine-readable; no shell hacks.
  • The first new contributor adding cursor or windsurf ships their format end-to-end without touching any flag definitions.

Scope and milestones

v0.14 (this issue):

  • Schema: outputs: accepts map form; list form continues to parse with deprecation warning
  • apm marketplace init writes explicit outputs: {claude: {}} map
  • apm pack --marketplace=<list> filter (comma + repeatable)
  • apm pack --marketplace=all and =none sentinels
  • apm pack --output-path <format>=<path> repeatable flag
  • APM_MARKETPLACE_<FORMAT>_PATH env support
  • apm pack --json machine-readable output
  • --marketplace-output emits real logger.warning(...) (also tracked in linked removal issue) and auto-translates to --output-path claude=PATH
  • Failure messages: unknown format, configured-but-missing format
  • Docs: cli-commands.md, pack.md, publish-to-a-marketplace.md
  • Test coverage: filter combinations, JSON shape stability, env-var precedence, deprecation warning

v0.15 (linked tracker issue):

  • Remove --marketplace-output flag entirely

Out of scope

  • Exclusion syntax (--marketplace='!claude') — defer until first user request
  • Multiple apm.yml files / config inheritance — keep one manifest, one source of truth
  • apm pack --format-help flag listing supported marketplace formats — nice-to-have, not a v0.14 blocker

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cliCLI command surface, flags, help text (cross-cutting).area/docs-sitedocs/src/content (Starlight), README, doc generation.area/marketplacemarketplace.json schema, federation, authoring suite, source parity.dxpriority/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