feat(marketplace): rename CTA + expand submit-flow guides (#308)
Three coordinated tweaks to the publication discovery surface: 1. Action-row CTA on /marketplace?tab=curated reads 'Submit a skill or plugin' instead of 'Submit a plugin'. Skills are first-class citizens of the curated shelf; the old wording made them feel like an afterthought. Same rename in the empty-state JS innerHTML so the two paths can't drift. 2. Curated guide page (/marketplace/guide/curated) expanded from a 4-line stub into a 3-step ordered list documenting the Named Curator handoff (find curator → handoff → publish + lifecycle). New '.guide-fastpath' callout block points users at the Flea Market when they want lighter review-bar / faster path. Primary CTA at the bottom of the curated guide now links to the flea guide too, so users who skim past the fast-path callout still see the escape hatch. 3. Flea guide page (/marketplace/guide/flea) expanded from a 3-line stub into a 4-step ordered list (package → upload via form → automated review → published). Documents the actual /store/new flow + the automated guardrails (manifest, content quality, prompt-injection scan) so users know what 'self-service' actually means before they upload. Route titles updated to match: 'Submit a skill or plugin to Curated Marketplace'. New file: tests/test_web_marketplace_guide.py — three tests covering the CTA rename, the curated guide's structural elements (Named Curators lede, 3 steps, fastpath callout, primary-CTA href), and the flea guide's structural elements (4 steps, no fastpath asymmetry, /store/new primary CTA).
This commit is contained in:
parent
17159bfad9
commit
cb13f80241
4 changed files with 280 additions and 25 deletions
|
|
@ -1525,7 +1525,7 @@ async def marketplace_guide_curated(
|
||||||
):
|
):
|
||||||
ctx = _build_context(
|
ctx = _build_context(
|
||||||
request, user=user,
|
request, user=user,
|
||||||
guide_title="Submit a plugin to Curated Marketplace",
|
guide_title="Submit a skill or plugin to Curated Marketplace",
|
||||||
guide_kind="curated",
|
guide_kind="curated",
|
||||||
)
|
)
|
||||||
return templates.TemplateResponse(request, "marketplace_guide.html", ctx)
|
return templates.TemplateResponse(request, "marketplace_guide.html", ctx)
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mp-actions">
|
<div class="mp-actions">
|
||||||
<a class="btn btn-secondary" data-actions-for="curated" href="/marketplace/guide/curated">Submit a plugin</a>
|
<a class="btn btn-secondary" data-actions-for="curated" href="/marketplace/guide/curated">Submit a skill or plugin</a>
|
||||||
<!-- Flea has a self-service +Upload button below — no second
|
<!-- Flea has a self-service +Upload button below — no second
|
||||||
"how to" CTA needed. Anyone uploads via the form directly. -->
|
"how to" CTA needed. Anyone uploads via the form directly. -->
|
||||||
<a class="btn btn-primary" data-actions-for="flea" href="/store/new" hidden>+ Upload</a>
|
<a class="btn btn-primary" data-actions-for="flea" href="/store/new" hidden>+ Upload</a>
|
||||||
|
|
@ -779,7 +779,7 @@ function fillEmptyState() {
|
||||||
if (state.tab === 'curated') {
|
if (state.tab === 'curated') {
|
||||||
t.textContent = 'No curated plugins available.';
|
t.textContent = 'No curated plugins available.';
|
||||||
b.textContent = 'Ask your admin to grant your group access.';
|
b.textContent = 'Ask your admin to grant your group access.';
|
||||||
c.innerHTML = `<a href="/marketplace/guide/curated">Submit a plugin →</a>`;
|
c.innerHTML = `<a href="/marketplace/guide/curated">Submit a skill or plugin →</a>`;
|
||||||
} else if (state.tab === 'flea') {
|
} else if (state.tab === 'flea') {
|
||||||
t.textContent = 'No community entities yet.';
|
t.textContent = 'No community entities yet.';
|
||||||
b.textContent = 'Be the first to share something.';
|
b.textContent = 'Be the first to share something.';
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,39 @@
|
||||||
/* Flea variant uses purple step bubbles (matches flea badge accent). */
|
/* Flea variant uses purple step bubbles (matches flea badge accent). */
|
||||||
.guide-page[data-kind="flea"] .guide-steps li::before { background: #6D28D9; }
|
.guide-page[data-kind="flea"] .guide-steps li::before { background: #6D28D9; }
|
||||||
|
|
||||||
|
/* Fast-path callout — visually distinct from the numbered steps so
|
||||||
|
readers spot "wait, there's a quicker option" without re-reading
|
||||||
|
the whole page. Lives only on the curated guide; flea has no
|
||||||
|
equivalent escape hatch (it's already the fast path). */
|
||||||
|
.guide-fastpath {
|
||||||
|
margin: 16px 0 28px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
background: linear-gradient(135deg, rgba(109, 40, 217, 0.06), rgba(109, 40, 217, 0.02));
|
||||||
|
border: 1px solid rgba(109, 40, 217, 0.25);
|
||||||
|
border-left: 4px solid #6D28D9;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.guide-fastpath h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 15px; font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.guide-fastpath p {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 14px; line-height: 1.6;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.guide-fastpath p strong { color: var(--text-primary); }
|
||||||
|
.guide-fastpath a.fastpath-cta {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: #6D28D9; color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px; font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.guide-fastpath a.fastpath-cta:hover { background: #5B21B6; }
|
||||||
|
|
||||||
/* ── Section block ──────────────────────────────────────────────── */
|
/* ── Section block ──────────────────────────────────────────────── */
|
||||||
.guide-section {
|
.guide-section {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
|
|
@ -130,32 +163,114 @@
|
||||||
<h1>{{ guide_title }}</h1>
|
<h1>{{ guide_title }}</h1>
|
||||||
|
|
||||||
{% if guide_kind == 'curated' %}
|
{% if guide_kind == 'curated' %}
|
||||||
{# Curated submission guide — body intentionally left minimal; full
|
{# Curated submission guide — publish-by-curator flow. Named Curators
|
||||||
content to be authored separately. The CTA buttons at the bottom of
|
are the gatekeepers (security / telemetry hygiene / docs bar). The
|
||||||
the page still let the user jump back to the Curated tab or browse
|
fast-path callout points users at the Flea Market when they want
|
||||||
the Flea Market as a self-service alternative. #}
|
reach without the review wait. #}
|
||||||
<p class="lede">
|
<p class="lede">
|
||||||
The Curated Marketplace is publish-by-curator only. To get your skill,
|
Curated Marketplace submissions go through <strong>Named Curators</strong>
|
||||||
agent, or plugin listed, hand it off to a curator who reviews and
|
— folks accountable for keeping the bar (security, telemetry hygiene,
|
||||||
publishes it on your behalf.
|
documentation) consistent across what ships on this shelf.
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
|
||||||
{# Flea publishing guide — body intentionally left minimal; full content
|
|
||||||
to be authored separately. The CTA buttons at the bottom of the page
|
|
||||||
still let the user jump to /store/new or browse the Flea Market. #}
|
|
||||||
<p class="lede">
|
|
||||||
The Flea Market is self-service. Anyone in the company can upload a
|
|
||||||
skill, agent, or plugin.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="guide-cta">
|
<ol class="guide-steps">
|
||||||
<a class="primary" href="{{ '/store/new' if guide_kind == 'flea' else '/marketplace?tab=curated' }}">
|
<li>
|
||||||
{{ '+ Upload to Flea Market' if guide_kind == 'flea' else '← Back to Curated' }}
|
<strong>Find a Curator.</strong>
|
||||||
</a>
|
Each plugin in the Curated Marketplace lists its curator on its detail
|
||||||
<a href="/marketplace?tab={{ 'flea' if guide_kind == 'curated' else 'curated' }}">
|
page. Reach out to any curator whose published work overlaps with what
|
||||||
{{ 'Browse Flea Market' if guide_kind == 'curated' else 'Browse Curated' }} →
|
you're shipping — they're the most likely owner. The
|
||||||
|
<em>See all curators →</em> link on the
|
||||||
|
<a href="/marketplace?tab=curated">Curated tab</a> is the directory entry.
|
||||||
|
<span class="note">If no curator's domain matches, ask the
|
||||||
|
{{ instance_brand }} platform team — they'll either adopt the
|
||||||
|
submission or steer it to the right person.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Hand off your skill or plugin.</strong>
|
||||||
|
Share the source repo (or a zipped bundle) plus a one-paragraph intro
|
||||||
|
covering <em>what it does</em>, <em>who it's for</em>, and
|
||||||
|
<em>what data it touches</em>. The curator runs it through the review
|
||||||
|
bar (manifest sanity, prompt-injection surface, doc completeness,
|
||||||
|
telemetry posture).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Curator publishes.</strong>
|
||||||
|
Once they accept, the curator commits the manifest entry to the
|
||||||
|
curated repo. Your plugin appears on the Curated tab on the next
|
||||||
|
nightly catalog sync (within ~24 h of merge).
|
||||||
|
<span class="note">Iteration after publish goes through the same
|
||||||
|
curator — they own the entry's lifecycle (version bumps,
|
||||||
|
deprecation, takedown).</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="guide-fastpath">
|
||||||
|
<h3>⚡ Want it live in minutes instead of days?</h3>
|
||||||
|
<p>
|
||||||
|
The <strong>Flea Market</strong> is the self-service shelf — anyone in
|
||||||
|
the company can upload a skill, agent, or plugin without going
|
||||||
|
through a curator. Same Claude Code reach, faster path, lighter
|
||||||
|
review bar (automated guardrails only; no human gatekeeper).
|
||||||
|
</p>
|
||||||
|
<a class="fastpath-cta" href="/marketplace/guide/flea">
|
||||||
|
Submit a skill to Flea Market →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="guide-cta">
|
||||||
|
<a class="primary" href="/marketplace/guide/flea">Submit a skill to Flea Market →</a>
|
||||||
|
<a href="/marketplace?tab=curated">← Back to Curated</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{# Flea publishing guide — self-service flow. No human gatekeeper,
|
||||||
|
automated guardrails only (content quality + security review run
|
||||||
|
server-side at submission time). Lives at /store/new. #}
|
||||||
|
<p class="lede">
|
||||||
|
The Flea Market is self-service. Anyone in the company can upload a
|
||||||
|
skill, agent, or plugin — no curator handoff, no review queue.
|
||||||
|
Automated guardrails check the manifest, content quality, and
|
||||||
|
prompt-injection surface at submission time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol class="guide-steps">
|
||||||
|
<li>
|
||||||
|
<strong>Package what you're shipping.</strong>
|
||||||
|
A skill is a single <code>SKILL.md</code> (front-matter + body); an
|
||||||
|
agent is an <code>AGENT.md</code>; a plugin is a folder with a
|
||||||
|
<code>plugin.json</code> manifest and any commands / agents / skills
|
||||||
|
it bundles. Zip the folder (or point at a git URL containing it).
|
||||||
|
<span class="note">If you're starting fresh, the
|
||||||
|
<a href="/marketplace/format-guide">format guide</a> documents each
|
||||||
|
shape with copy-pasteable examples.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Upload via the form.</strong>
|
||||||
|
Head to <a href="/store/new"><code>/store/new</code></a>. Drop the
|
||||||
|
zip (or paste the git URL), fill the metadata form (name, short
|
||||||
|
description, what data it touches, who it's for), submit.
|
||||||
|
<span class="note">The <em>+ Upload</em> button on the Flea Market
|
||||||
|
tab is the same form.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Automated review.</strong>
|
||||||
|
Server runs content-quality checks (manifest completeness, prose
|
||||||
|
length, distinct-word floor) and a prompt-injection scan. Most
|
||||||
|
submissions pass within seconds. Failures surface inline with the
|
||||||
|
exact field to fix.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Published.</strong>
|
||||||
|
Once green, your submission goes live on the Flea Market tab
|
||||||
|
immediately — no nightly-sync delay. Iteration goes through the
|
||||||
|
same form: upload a new version, the form recognises the slug
|
||||||
|
and bumps the version pointer.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="guide-cta">
|
||||||
|
<a class="primary" href="/store/new">+ Upload to Flea Market</a>
|
||||||
|
<a href="/marketplace?tab=curated">Browse Curated →</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
140
tests/test_web_marketplace_guide.py
Normal file
140
tests/test_web_marketplace_guide.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
"""GET /marketplace/guide/{curated,flea} — submission flow guides.
|
||||||
|
|
||||||
|
Both routes are authed (`get_current_user` dependency). The curated guide
|
||||||
|
documents the Named Curator handoff and has a fast-path callout pointing
|
||||||
|
at the flea self-service guide; the flea guide documents the /store/new
|
||||||
|
upload flow. Together with the action-row CTA on /marketplace?tab=curated,
|
||||||
|
this trio is the discovery surface for "how do I get my plugin published".
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fresh_db(monkeypatch):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
monkeypatch.setenv("DATA_DIR", tmp)
|
||||||
|
monkeypatch.setenv("TESTING", "1")
|
||||||
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!")
|
||||||
|
yield tmp
|
||||||
|
|
||||||
|
|
||||||
|
def _make_user_and_session(conn, email="u@example.com"):
|
||||||
|
from src.repositories.users import UserRepository
|
||||||
|
from app.auth.jwt import create_access_token
|
||||||
|
|
||||||
|
uid = str(uuid.uuid4())
|
||||||
|
UserRepository(conn).create(id=uid, email=email, name=email.split("@")[0])
|
||||||
|
return uid, create_access_token(user_id=uid, email=email)
|
||||||
|
|
||||||
|
|
||||||
|
def _client():
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_marketplace_curated_tab_cta_text(fresh_db):
|
||||||
|
"""The action-row CTA on /marketplace?tab=curated reads
|
||||||
|
'Submit a skill or plugin' (renamed from 'Submit a plugin' so skills
|
||||||
|
aren't an afterthought) and links to the curated guide. Empty-state
|
||||||
|
fallback in JS uses the same string so both surfaces stay in sync."""
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
body = _client().get(
|
||||||
|
"/marketplace?tab=curated", cookies={"access_token": sess}
|
||||||
|
).text
|
||||||
|
|
||||||
|
# Action-row anchor — primary discovery path.
|
||||||
|
assert (
|
||||||
|
'<a class="btn btn-secondary" data-actions-for="curated" '
|
||||||
|
'href="/marketplace/guide/curated">Submit a skill or plugin</a>'
|
||||||
|
) in body
|
||||||
|
# Empty-state JS innerHTML — same string, no drift.
|
||||||
|
assert "Submit a skill or plugin →" in body
|
||||||
|
# Old wording must be gone — guards against partial rename.
|
||||||
|
assert ">Submit a plugin<" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_marketplace_guide_curated_page(fresh_db):
|
||||||
|
"""Curated guide page documents the Named Curator handoff. Three-step
|
||||||
|
flow (find → handoff → publish) lives inside `.guide-steps`. The
|
||||||
|
fast-path callout points users at the flea guide as the lighter
|
||||||
|
review-bar alternative; the primary CTA at the bottom does the same
|
||||||
|
so users who skim past the callout still see the escape hatch."""
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
resp = _client().get(
|
||||||
|
"/marketplace/guide/curated", cookies={"access_token": sess}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.text
|
||||||
|
|
||||||
|
# Title carries the new 'skill or plugin' wording.
|
||||||
|
assert "Submit a skill or plugin to Curated Marketplace" in body
|
||||||
|
# Lede surfaces the gatekeeping concept.
|
||||||
|
assert "Named Curators" in body
|
||||||
|
# Three-step ordered list under `.guide-steps`.
|
||||||
|
assert '<ol class="guide-steps">' in body
|
||||||
|
assert "Find a Curator" in body
|
||||||
|
assert "Hand off your skill or plugin" in body
|
||||||
|
assert "Curator publishes" in body
|
||||||
|
# Fast-path callout exists and the CTA inside it points at the flea
|
||||||
|
# guide (NOT /store/new directly — we want users to read the flea
|
||||||
|
# context before they upload).
|
||||||
|
assert '<div class="guide-fastpath">' in body
|
||||||
|
assert 'href="/marketplace/guide/flea"' in body
|
||||||
|
# Primary CTA at the bottom also surfaces the flea path.
|
||||||
|
assert 'class="primary" href="/marketplace/guide/flea"' in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_marketplace_guide_flea_page(fresh_db):
|
||||||
|
"""Flea guide documents the /store/new self-service flow. Four-step
|
||||||
|
body (package → upload → automated review → published) replaces the
|
||||||
|
earlier stub. Primary CTA goes directly to /store/new since users
|
||||||
|
landing on the flea guide have already chosen the self-service path."""
|
||||||
|
from src.db import get_system_db, close_system_db
|
||||||
|
|
||||||
|
conn = get_system_db()
|
||||||
|
try:
|
||||||
|
_, sess = _make_user_and_session(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
close_system_db()
|
||||||
|
resp = _client().get(
|
||||||
|
"/marketplace/guide/flea", cookies={"access_token": sess}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.text
|
||||||
|
|
||||||
|
assert "Upload to Flea Market" in body
|
||||||
|
# Four-step ordered list (no fast-path callout on flea — it IS the
|
||||||
|
# fast path, the curated guide is what links here).
|
||||||
|
assert '<ol class="guide-steps">' in body
|
||||||
|
assert "Package what you" in body
|
||||||
|
assert "Upload via the form" in body
|
||||||
|
assert "Automated review" in body
|
||||||
|
assert "Published" in body
|
||||||
|
# Primary CTA goes straight to /store/new (flea is one click away
|
||||||
|
# from being live, no intermediate handoff).
|
||||||
|
assert 'class="primary" href="/store/new"' in body
|
||||||
|
# No fast-path callout here — sanity check the asymmetry sticks.
|
||||||
|
assert '<div class="guide-fastpath">' not in body
|
||||||
Loading…
Reference in a new issue