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
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.
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.
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.
Magic-string registry. Adding cursor / windsurf / gemini later means another round of CLI-flag bikeshedding (--cursor-output? --marketplace-cursor? rename the existing flag again?).
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 listclaude: {} # path defaults to .claude-plugin/marketplace.jsoncodex:
path: .agents/plugins/marketplace.jsonpackages:
- name: foosource: ./plugins/foocategory: 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.
This is the killer CI feature — jq -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.
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
Marketplace output UX: filter-driven CLI + map-based manifest + JSON output
TL;DR
Refactor the
apm packmarketplace 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):
Three concrete UX bugs:
apm pack --marketplace-output Xreads as "set the marketplace path" but only touches Claude. CI scripts adoptingoutputs:[claude,codex]will silently drop the Codex artifact override, with no warning.apm.yml. Matrix builds devolve into shell hacks or per-job manifest copies.cursor/windsurf/geminilater 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"| targetProposed UX
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 —
outputsis a mappath, format-specific required fields) lives inside the entry. Eliminates the dual-source confusion (outputs: [claude, codex]selector + separateclaude:+codex:sibling blocks).outputs: [claude, codex](list form) keeps parsing as shorthand for{claude: {}, codex: {}}; emits a one-cycle deprecation warning.apm marketplace initwritesoutputs: {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
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
Or via env (12-factor / secrets-friendly):
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
{ "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 feature —
jq -r '.outputs[].path'and pipe toactions/upload-artifactorgh release upload. Zero hardcoded paths in workflows.5. Real deprecation for
--marketplace-outputWhen the legacy flag is passed:
Behavior auto-translates to
--output-path claude=Xfor one minor cycle, then removed. Tracker: see linked removal issue.6. Failure mode is the product
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 ..."]Adversarial review — build-tool peers
docker buildx --platform[claude]/[codex]already in #1281.goreleaser --id--jsonoutput, which goreleaser users routinely wish for.webpack --config-name--config-name='!claude'exclusion. Resolution: not v1; YAGNI. Comma list covers 95%; ship without exclusion until first user asks.cargo --target--targetis single-value, repeated via shell loops. Resolution: APM accepts both comma-separated AND repeatable--marketplace foo --marketplace bar. Clickmultiple=Truehandles it.rollupconfig-driven outputs--formatin favor of config arrays — exactly the direction we're going.hugo outputs:--marketplace=is a strict superset for the CI matrix use case.vitemulti-config filesnpm publish --tagapm packbuilds whateveroutputs:declares — never magical "all formats supported by APM."initwrites the explicit{claude: {}}so the default is observable in the file.terraform -target-targetis 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):
Single job, all formats, fan-out upload:
Local dev (fast iteration on one format):
Day-one when
cursorships — zero CLI changes:apm pack --marketplace=cursor --json # works immediatelysequenceDiagram 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 workflowWhy 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:
apm.ymlreview.cursororwindsurfships their format end-to-end without touching any flag definitions.Scope and milestones
v0.14 (this issue):
outputs:accepts map form; list form continues to parse with deprecation warningapm marketplace initwrites explicitoutputs: {claude: {}}mapapm pack --marketplace=<list>filter (comma + repeatable)apm pack --marketplace=alland=nonesentinelsapm pack --output-path <format>=<path>repeatable flagAPM_MARKETPLACE_<FORMAT>_PATHenv supportapm pack --jsonmachine-readable output--marketplace-outputemits reallogger.warning(...)(also tracked in linked removal issue) and auto-translates to--output-path claude=PATHcli-commands.md,pack.md,publish-to-a-marketplace.mdv0.15 (linked tracker issue):
--marketplace-outputflag entirelyOut of scope
--marketplace='!claude') — defer until first user requestapm.ymlfiles / config inheritance — keep one manifest, one source of truthapm pack --format-helpflag listing supported marketplace formats — nice-to-have, not a v0.14 blockerReferences
--marketplace-output--marketplace-outputflag in v0.15 #1318 — v0.15 tracker for removal of--marketplace-output