diff --git a/app/web/router.py b/app/web/router.py index c8c68ed..cdffd49 100644 --- a/app/web/router.py +++ b/app/web/router.py @@ -1525,7 +1525,7 @@ async def marketplace_guide_curated( ): ctx = _build_context( request, user=user, - guide_title="Submit a plugin to Curated Marketplace", + guide_title="Submit a skill or plugin to Curated Marketplace", guide_kind="curated", ) return templates.TemplateResponse(request, "marketplace_guide.html", ctx) diff --git a/app/web/templates/marketplace.html b/app/web/templates/marketplace.html index 2da00a0..48b00f9 100644 --- a/app/web/templates/marketplace.html +++ b/app/web/templates/marketplace.html @@ -440,7 +440,7 @@
- Submit a plugin + Submit a skill or plugin @@ -779,7 +779,7 @@ function fillEmptyState() { if (state.tab === 'curated') { t.textContent = 'No curated plugins available.'; b.textContent = 'Ask your admin to grant your group access.'; - c.innerHTML = `Submit a plugin →`; + c.innerHTML = `Submit a skill or plugin →`; } else if (state.tab === 'flea') { t.textContent = 'No community entities yet.'; b.textContent = 'Be the first to share something.'; diff --git a/app/web/templates/marketplace_guide.html b/app/web/templates/marketplace_guide.html index 1602b7b..74de95c 100644 --- a/app/web/templates/marketplace_guide.html +++ b/app/web/templates/marketplace_guide.html @@ -71,6 +71,39 @@ /* Flea variant uses purple step bubbles (matches flea badge accent). */ .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 ──────────────────────────────────────────────── */ .guide-section { background: var(--card-bg); @@ -130,32 +163,114 @@

{{ guide_title }}

{% if guide_kind == 'curated' %} - {# Curated submission 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 back to the Curated tab or browse - the Flea Market as a self-service alternative. #} + {# Curated submission guide — publish-by-curator flow. Named Curators + are the gatekeepers (security / telemetry hygiene / docs bar). The + fast-path callout points users at the Flea Market when they want + reach without the review wait. #}

- The Curated Marketplace is publish-by-curator only. To get your skill, - agent, or plugin listed, hand it off to a curator who reviews and - publishes it on your behalf. + Curated Marketplace submissions go through Named Curators + — folks accountable for keeping the bar (security, telemetry hygiene, + documentation) consistent across what ships on this shelf.

- {% 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. #} -

- The Flea Market is self-service. Anyone in the company can upload a - skill, agent, or plugin. -

- {% endif %} -
- - {{ '+ Upload to Flea Market' if guide_kind == 'flea' else '← Back to Curated' }} - - - {{ 'Browse Flea Market' if guide_kind == 'curated' else 'Browse Curated' }} → +
    +
  1. + Find a Curator. + Each plugin in the Curated Marketplace lists its curator on its detail + page. Reach out to any curator whose published work overlaps with what + you're shipping — they're the most likely owner. The + See all curators → link on the + Curated tab is the directory entry. + 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. +
  2. +
  3. + Hand off your skill or plugin. + Share the source repo (or a zipped bundle) plus a one-paragraph intro + covering what it does, who it's for, and + what data it touches. The curator runs it through the review + bar (manifest sanity, prompt-injection surface, doc completeness, + telemetry posture). +
  4. +
  5. + Curator publishes. + 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). + Iteration after publish goes through the same + curator — they own the entry's lifecycle (version bumps, + deprecation, takedown). +
  6. +
+ +
+

⚡ Want it live in minutes instead of days?

+

+ The Flea Market 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). +

+ + Submit a skill to Flea Market →
+ + + {% 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. #} +

+ 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. +

+ +
    +
  1. + Package what you're shipping. + A skill is a single SKILL.md (front-matter + body); an + agent is an AGENT.md; a plugin is a folder with a + plugin.json manifest and any commands / agents / skills + it bundles. Zip the folder (or point at a git URL containing it). + If you're starting fresh, the + format guide documents each + shape with copy-pasteable examples. +
  2. +
  3. + Upload via the form. + Head to /store/new. Drop the + zip (or paste the git URL), fill the metadata form (name, short + description, what data it touches, who it's for), submit. + The + Upload button on the Flea Market + tab is the same form. +
  4. +
  5. + Automated review. + 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. +
  6. +
  7. + Published. + 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. +
  8. +
+ + + {% endif %}
{% endblock %} diff --git a/tests/test_web_marketplace_guide.py b/tests/test_web_marketplace_guide.py new file mode 100644 index 0000000..bed4dad --- /dev/null +++ b/tests/test_web_marketplace_guide.py @@ -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 ( + 'Submit a skill or plugin' + ) 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 '
    ' 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 '
    ' 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 '
      ' 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 '
      ' not in body