diff --git a/README.md b/README.md index 1d76877..f3b1222 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,6 @@ Marketplace plugins for Reforma. Each plugin is a folder with `.reforma-plugin/p | react-best-practices | [React Best Practices](marketplace/playbooks/react-best-practices) | Vercel | Playbooks | Keep the app fast — React and Next.js performance patterns | | i18n | [i18n](marketplace/playbooks/i18n) | Reforma | Playbooks | Multiple languages — translated copy and a language switcher for the app | | booking | [Booking](marketplace/playbooks/booking) | Reforma | Playbooks | Book a call, pick a slot, or show an event calendar — no third-party account | -| shadcn-tailwind | [shadcn / Tailwind](marketplace/system/shadcn-tailwind) | shadcn | System | Skills and MCP that help build components with shadcn and Tailwind. | -| visuals | [Visuals](marketplace/system/visuals) | Reforma | System | Type, color, marks, and icons for this product | -| nextjs | [Next.js](marketplace/system/nextjs) | Reforma | System | App Router — routes, Server Actions, caching, and proxy.ts | | coss | [coss](marketplace/ui/coss) | coss | UI | Extra components and particle effects beyond the default shadcn set | | flexnative | [Flexnative](marketplace/ui/flexnative) | Flexnative | UI | Ready-made page blocks and patterns — drop in a section instead of assembling it | | magic-ui | [Magic UI](marketplace/ui/magic-ui) | Magic UI | UI | Animated marketing pieces — shine, marquee, shimmer, and that kind of polish | @@ -33,7 +30,7 @@ Marketplace plugins for Reforma. Each plugin is a folder with `.reforma-plugin/p | dropbox | [Dropbox](https://github.com/dropbox/dropbox-ai-plugins/tree/main/codex) | Dropbox | Files | Files in Dropbox — save, share, and pull them into the project | | cloudinary | [Cloudinary](marketplace/files/cloudinary) | Cloudinary | Files | Images and video in the cloud — upload, transform, and deliver them | | slack | [Slack](marketplace/workspace/slack) | Slack | Workspace | The team’s Slack — channels, threads, and posts from the project | -| github-mcp | [GitHub](marketplace/workspace/github-mcp) | GitHub | Workspace | GitHub in the project — issues, pull requests, and the repo | +| github | [GitHub](marketplace/workspace/github) | GitHub | Workspace | GitHub in the project — issues, pull requests, and the repo | | clerk | [Clerk](marketplace/auth/clerk) | Clerk | Auth | Drop-in auth — sign-in, organizations, and user accounts for the app | | supabase | [Supabase](https://github.com/supabase-community/supabase-plugin) | Supabase | Backend | Backend for the app — database, auth, and file storage | | stripe | [Stripe](https://github.com/stripe/ai/tree/main/providers/cursor/plugin) | Stripe | Payments | Accept payments, manage customers, and handle subscriptions | diff --git a/marketplace.json b/marketplace.json index 7c0b427..af82a45 100644 --- a/marketplace.json +++ b/marketplace.json @@ -1,23 +1,5 @@ { "categories": [ - { - "id": "system", - "name": "System", - "plugins": [ - { - "name": "shadcn-tailwind", - "source": "./marketplace/system/shadcn-tailwind" - }, - { - "name": "visuals", - "source": "./marketplace/system/visuals" - }, - { - "name": "nextjs", - "source": "./marketplace/system/nextjs" - } - ] - }, { "id": "moods", "name": "Moods", @@ -118,7 +100,10 @@ "source": "https://github.com/motiondivision/cursor-plugin/tree/1140efe9ad5e03c689ea6bb19d9d3850a4dae5f7/plugins/motion", "displayName": "Motion", "description": "Animation for the web — springs, gestures, and layout motion", - "brandColor": "#000000" + "brandColor": "#000000", + "agent": { + "mentions": [] + } }, { "name": "remotion", @@ -149,7 +134,10 @@ "source": "https://github.com/linear/cursor-plugin/tree/c2c4cb2ab23206c9219b0dd31c9571e4c922faeb", "displayName": "Linear", "description": "The issue tracker — bugs, projects, and docs from Linear", - "brandColor": "#000000" + "brandColor": "#000000", + "agent": { + "mentions": ["linear.app"] + } }, { "name": "slack", @@ -157,7 +145,7 @@ }, { "name": "github", - "source": "./marketplace/workspace/github-mcp" + "source": "./marketplace/workspace/github" } ] }, diff --git a/marketplace/automation/make/.reforma-plugin/plugin.json b/marketplace/automation/make/.reforma-plugin/plugin.json index deb3ce7..0a5388c 100644 --- a/marketplace/automation/make/.reforma-plugin/plugin.json +++ b/marketplace/automation/make/.reforma-plugin/plugin.json @@ -7,6 +7,9 @@ }, "mcpServers": "./mcp.json", "logo": "assets/logo.svg", + "agent": { + "mentions": ["make.com"] + }, "interface": { "displayName": "Make", "brandColor": "#6D00CC", diff --git a/marketplace/backend/neon/.reforma-plugin/plugin.json b/marketplace/backend/neon/.reforma-plugin/plugin.json index b533e00..41eb4a1 100644 --- a/marketplace/backend/neon/.reforma-plugin/plugin.json +++ b/marketplace/backend/neon/.reforma-plugin/plugin.json @@ -7,6 +7,9 @@ }, "mcpServers": "./mcp.json", "logo": "assets/logo.svg", + "agent": { + "mentions": ["neon.tech"] + }, "interface": { "displayName": "Neon", "brandColor": "#00E599", diff --git a/marketplace/media/fal/.reforma-plugin/plugin.json b/marketplace/media/fal/.reforma-plugin/plugin.json index e68f7d1..598629c 100644 --- a/marketplace/media/fal/.reforma-plugin/plugin.json +++ b/marketplace/media/fal/.reforma-plugin/plugin.json @@ -6,6 +6,9 @@ "url": "https://fal.ai" }, "mcpServers": "./mcp.json", + "agent": { + "mentions": ["fal.ai"] + }, "variables": { "type": "object", "properties": { diff --git a/marketplace/playbooks/frontend-design/.reforma-plugin/plugin.json b/marketplace/playbooks/frontend-design/.reforma-plugin/plugin.json index c7259ca..579442a 100644 --- a/marketplace/playbooks/frontend-design/.reforma-plugin/plugin.json +++ b/marketplace/playbooks/frontend-design/.reforma-plugin/plugin.json @@ -9,9 +9,6 @@ "homepage": "https://github.com/anthropics/skills/tree/main/skills/frontend-design", "repository": "https://github.com/anthropics/skills", "license": "Apache-2.0", - "agent": { - "discover": true - }, "interface": { "displayName": "Frontend Design", "brandColor": "#000000", diff --git a/marketplace/playbooks/react-best-practices/.reforma-plugin/plugin.json b/marketplace/playbooks/react-best-practices/.reforma-plugin/plugin.json index b261e8a..0373832 100644 --- a/marketplace/playbooks/react-best-practices/.reforma-plugin/plugin.json +++ b/marketplace/playbooks/react-best-practices/.reforma-plugin/plugin.json @@ -10,7 +10,6 @@ "repository": "https://github.com/vercel-labs/agent-skills", "license": "MIT", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/system/nextjs/.reforma-plugin/plugin.json b/marketplace/system/nextjs/.reforma-plugin/plugin.json deleted file mode 100644 index d024dee..0000000 --- a/marketplace/system/nextjs/.reforma-plugin/plugin.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "nextjs", - "description": "App Router — routes, Server Actions, caching, and proxy.ts", - "system": true, - "logo": "assets/logo.svg", - "homepage": "https://nextjs.org/docs/app", - "interface": { - "displayName": "Next.js", - "brandColor": "#000000", - "logo": "assets/logo.svg", - "logoSmall": "assets/logo-small.svg" - }, - "logoSmall": "assets/logo-small.svg" -} diff --git a/marketplace/system/nextjs/assets/logo-small.svg b/marketplace/system/nextjs/assets/logo-small.svg deleted file mode 100644 index 7167462..0000000 --- a/marketplace/system/nextjs/assets/logo-small.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/marketplace/system/nextjs/assets/logo.svg b/marketplace/system/nextjs/assets/logo.svg deleted file mode 100644 index d7f9b15..0000000 --- a/marketplace/system/nextjs/assets/logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/marketplace/system/nextjs/skills/nextjs/SKILL.md b/marketplace/system/nextjs/skills/nextjs/SKILL.md deleted file mode 100644 index 43f9518..0000000 --- a/marketplace/system/nextjs/skills/nextjs/SKILL.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: nextjs -disable-user-invocation: true -description: >- - Next.js App Router routes, layouts, loading and error UI, metadata, - request APIs, Server Actions, route handlers, caching, authentication, - and request rewrites. ---- - -# Next.js - -## Before editing - -Read `package.json` → `next`, `next.config.ts`, and the existing App Router tree (`app/` or `src/app`). Match adjacent routes for naming, runtime, caching, errors, providers, data-access, and auth. One App Router root. Leave `fonts/` and `tokens/` in place unless the task moves them. - -When a sibling skill exists, read and apply it before changing that surface. Otherwise follow adjacent files. - -- UI primitives → `shadcn` -- Fonts → `fonts` -- Tokens → `tokens` -- Chrome icons → `icons` -- Marks / favicons → `svg` -- Locales → `i18n` -- Performance → `react-best-practices` - -Preserve `next.config.ts` composition and keys (`turbopack`, `tsconfigPath`, wrappers, plugins, environment branches). Run the project's `lint` script — Next 16 has no `next lint`. Leave `cacheComponents` as configured unless the user asked to change it. Leave `instrumentation-client.ts` unless the task is client instrumentation. - -## Route - -Add the route inside the existing route-group and layout structure. Route groups `(…)`, parallel slots `@slot`, and intercepting routes do not necessarily map to URL segments. - -- `page.tsx` — makes the route segment publicly addressable -- `layout.tsx` — nested layout only when the segment needs a persistent shared **boundary**: UI, providers, data, metadata, or parallel-route slots -- `loading.tsx` — fallback for the **whole** segment. One slow block: local `` -- `error.tsx` — Client Component. Does not catch errors in its own `layout.tsx`. Root layout failures: `global-error.tsx`. Expected form/mutation errors: typed action state -- `route.ts` — HTTP endpoint - -Static head: `export const metadata`. Head that needs params or cookies: `generateMetadata`. - -Nav: `next/link`, or `Link` from `@/i18n/navigation` when that module exists. User-facing raster images: `next/image` unless the source or context cannot use the optimizer. Keep existing remote image config. - -`notFound()`, `redirect()`, `permanentRedirect()` for those outcomes. `unauthorized()` / `forbidden()` only when this Next version and config support them. Do not swallow `redirect()` or `notFound()` in a broad `catch`. - -## Request data - -On Next 15+, request APIs are asynchronous: - -- `params` — Promise in `page.tsx`, `layout.tsx`, `generateMetadata`, and Route Handler context -- `searchParams` — Promise in `page.tsx` and `generateMetadata`. Layouts do not receive it -- `cookies()`, `headers()`, `draftMode()` — return Promises - -Await them, or `use()` a Promise in a Client Component. - -When this Next version generates them, type with `PageProps<'/users/[id]'>`, `LayoutProps<'/users/[id]'>`, `RouteContext<'/api/users/[id]'>`. - -Route handlers: `await context.params`. Query string is `request.nextUrl.searchParams`. - -```tsx -export default async function Page(props: PageProps<"/users/[id]">) { - const { id } = await props.params; - const query = await props.searchParams; -} -``` - -## Server vs client - -Default is a Server Component. Read data in Server Components or the server-side data-access layer, close to the owner, unless that creates a waterfall or fights the project's architecture. Pass serializable props into a small client **island**. Extend the existing client provider file. - -Add `'use client'` only at the client boundary. Modules imported exclusively through that boundary are already in the client graph. Keep the boundary low. Event handlers live inside the client graph — they cannot cross the Server-to-Client serialization boundary. - -Secrets stay on the server. Browser-exposed values: `NEXT_PUBLIC_`. Modules that must never enter the client graph: `import 'server-only'` when the project uses it. - -Call the data-access layer from Server Components and Server Actions. `fetch` this app's own Route Handlers only when an HTTP boundary is required. - -Start independent requests before awaiting them. Suspense streams independent UI. - -## Cache - -Inspect `cacheComponents` and adjacent conventions. - -When `cacheComponents` is on: `'use cache'` only for data safe to share at that scope; declare lifetime and tags on purpose; keep request-time APIs outside cached scopes unless the project uses private cache. - -Diagnose a failing fetch. Do not hide the cause with `force-dynamic`, `no-store`, or unrelated revalidation. - -## Mutations - -`'use server'` and `route.ts` are public. A layout or `proxy.ts` / `middleware.ts` check does not cover a direct POST. Mark intentionally public endpoints (webhooks, OAuth callbacks) and verify signatures/state there. - -Protected mutations, in order: authenticate → validate untrusted input → authorize the operation and referenced resources → write → invalidate affected data. Use the project's auth helper (and the auth skill when present). Add `@/lib/auth` only when that file already exists. - -- Route Handler: no session → `401`; session without permission → `403`; bad input → `400` / `422`; missing → `404`; conflict → `409` -- Server Action: stop before the write. Throw, `redirect`, or return the project's typed action state — match adjacent actions - -Invalidate **only the affected data**: - -- `updateTag` — Server Action expires tagged data immediately (read-your-own-writes) -- `revalidateTag` — tagged stale-while-revalidate (actions and handlers). Use the non-deprecated signature for the pinned Next version, not a profile copied from another repo -- `revalidatePath` — the dependency is the route. Valid in Server Actions and Route Handlers -- `refresh()` — Server Actions only. Refreshes the client router; it does not expire the server cache - -## Rewrites and Proxy - -Prefer `redirects()` / `rewrites()` in `next.config.ts` for declarative path, header, cookie, or query rules. Use `proxy.ts` for request-time logic that cannot be expressed there. - -On Next 16+, place `proxy.ts` at the same level as `app/` (`proxy.ts` or `src/proxy.ts`) and export `proxy`. Extend existing `middleware.ts` on Next 15; do not add a new one on Next 16. Locale interception: `i18n` skill when present. - -## Done - -The route renders or the handler responds correctly for every case it owns. Protected mutations authorize, return the right status or action state, and invalidate only the data they changed. Independent fetches are not waterfalls. `fonts/` / `tokens/` paths are unchanged unless the task moved them. The project's lint and type-check commands pass. diff --git a/marketplace/system/shadcn-tailwind/.reforma-plugin/plugin.json b/marketplace/system/shadcn-tailwind/.reforma-plugin/plugin.json deleted file mode 100644 index 3b84aaa..0000000 --- a/marketplace/system/shadcn-tailwind/.reforma-plugin/plugin.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "shadcn-tailwind", - "description": "Skills and MCP that help build components with shadcn and Tailwind", - "system": true, - "logo": "assets/logo.png", - "author": { - "name": "shadcn" - }, - "homepage": "https://ui.shadcn.com", - "repository": "https://github.com/shadcn-ui/ui", - "license": "MIT", - "interface": { - "displayName": "shadcn", - "brandColor": "#000000", - "logo": "assets/logo.png", - "logoSmall": "assets/logo-small.png" - }, - "logoSmall": "assets/logo-small.png" -} diff --git a/marketplace/system/shadcn-tailwind/assets/logo-small.png b/marketplace/system/shadcn-tailwind/assets/logo-small.png deleted file mode 100644 index 1027935..0000000 Binary files a/marketplace/system/shadcn-tailwind/assets/logo-small.png and /dev/null differ diff --git a/marketplace/system/shadcn-tailwind/assets/logo.png b/marketplace/system/shadcn-tailwind/assets/logo.png deleted file mode 100644 index 1027935..0000000 Binary files a/marketplace/system/shadcn-tailwind/assets/logo.png and /dev/null differ diff --git a/marketplace/system/shadcn-tailwind/mcp.json b/marketplace/system/shadcn-tailwind/mcp.json deleted file mode 100644 index 01ab230..0000000 --- a/marketplace/system/shadcn-tailwind/mcp.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "mcpServers": { - "shadcn": { - "command": "bunx", - "args": ["shadcn", "mcp"], - "description": "Search, view, and example-code items from registries in components.json. Add still goes through the CLI.", - "tools": { - "get_project_registries": { - "title": "Get project registries", - "description": "Registry names from components.json. Errors if that file is missing.", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - "list_items_in_registries": { - "title": "List registry items", - "description": "List items from registries in components.json.", - "inputSchema": { - "type": "object", - "properties": { - "registries": { - "type": "array", - "items": { "type": "string" }, - "description": "Registry names (e.g. ['@shadcn', '@coss']). Omit to list every configured registry." - }, - "types": { - "type": "array", - "items": { "type": "string" }, - "description": "Filter by type shorthand (ui, block, component, …)." - }, - "limit": { - "type": "number", - "description": "Max items (default 100; 0 = no limit)." - }, - "offset": { - "type": "number", - "description": "Items to skip." - } - } - } - }, - "search_items_in_registries": { - "title": "Search registry items", - "description": "Fuzzy search names and descriptions. Then get_item_examples_from_registries for usage.", - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Fuzzy match against item names and descriptions." - }, - "registries": { - "type": "array", - "items": { "type": "string" }, - "description": "Registry names (e.g. ['@shadcn']). Omit to search every configured registry." - }, - "types": { - "type": "array", - "items": { "type": "string" }, - "description": "Filter by type shorthand (ui, block, component, …)." - }, - "limit": { - "type": "number", - "description": "Max items (default 100; 0 = no limit)." - }, - "offset": { - "type": "number", - "description": "Items to skip." - } - }, - "required": ["query"] - } - }, - "view_items_in_registries": { - "title": "View registry items", - "description": "Name, description, type, and file contents. For demos use get_item_examples_from_registries.", - "inputSchema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { "type": "string" }, - "description": "Namespaced names (e.g. ['@shadcn/button', '@coss/alert'])." - } - }, - "required": ["items"] - } - }, - "get_item_examples_from_registries": { - "title": "Get item examples", - "description": "Demo code and dependencies. Query like 'button-demo' or 'card example'.", - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Example search, e.g. 'accordion-demo', 'button example'." - }, - "registries": { - "type": "array", - "items": { "type": "string" }, - "description": "Registry names. Omit to search every configured registry." - } - }, - "required": ["query"] - } - }, - "get_add_command_for_items": { - "title": "Get add command", - "description": "Returns the shadcn add CLI string. Does not install — run that command next.", - "inputSchema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { "type": "string" }, - "description": "Namespaced names (e.g. ['@shadcn/button'])." - } - }, - "required": ["items"] - } - }, - "get_audit_checklist": { - "title": "Get audit checklist", - "description": "Post-add checklist (imports, deps, lint, types).", - "inputSchema": { - "type": "object", - "properties": {} - } - } - } - } - } -} diff --git a/marketplace/system/shadcn-tailwind/skills/shadcn/SKILL.md b/marketplace/system/shadcn-tailwind/skills/shadcn/SKILL.md deleted file mode 100644 index a472611..0000000 --- a/marketplace/system/shadcn-tailwind/skills/shadcn/SKILL.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: shadcn -disable-user-invocation: true -description: >- - Installs and composes official shadcn/ui primitives. Use when a needed - component is missing from components/ui, when running the shadcn CLI or MCP, - or when editing components.json. ---- - -# shadcn - -Starter already has `components.json`, Tailwind 4, shadcn v4 (Base UI), and Reforma tokens. Do not re-init. Do not add a second UI framework. - -Official registry is the default. Other `@ns` kit registries only when that kit's card is installed — the kit **Install** hook runs `shadcn registry add`. CLI writes the namespace into `components.json` if that file exists, otherwise into `package.json`. - -Layout classes, `gap-*`, `size-*`: **Skill** `tailwind`. Colors: **Skill** `tokens`. - -## Find - -Prefer a matching local `components/ui/*` if it already exists. - -1. MCP `search_items_in_registries` — `query` plus optional `registries` / `types`. -2. MCP `view_items_in_registries` or `get_item_examples_from_registries` for files and demos. - -## Install - -1. **Bash:** `bunx shadcn add -y` (optional: `--dry-run` / `--diff` first). MCP `get_add_command_for_items` only returns the command — still run it. -2. If `package.json` / lockfile changed → **InstallDependencies**. -3. Read the added file. Match neighboring primitives — reuse their class patterns / compose with existing `Button` / `Input` / `Field`. `cn` from `@/lib/utils`. Colors: **Skill** `tokens`. Icons: **Skill** `icons`. - -## Compose - -Existing components first. Variants before custom classes. `className` on a primitive is layout (`max-w-md`, `mt-4`), not a color/type restyle. - -Forms: `FieldGroup` + `Field` (`data-invalid` on `Field`, `aria-invalid` on the control). Icons in `Button`: `data-icon="inline-start"` / `inline-end`, no sizing classes on the icon. Overlays need a Title (`DialogTitle`, `SheetTitle`, `DrawerTitle`). Do not set `z-*` on Dialog / Sheet / Drawer / Popover — they stack themselves. diff --git a/marketplace/system/shadcn-tailwind/skills/tailwind/SKILL.md b/marketplace/system/shadcn-tailwind/skills/tailwind/SKILL.md deleted file mode 100644 index e3c22e7..0000000 --- a/marketplace/system/shadcn-tailwind/skills/tailwind/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: tailwind -disable-user-invocation: true -description: >- - Tailwind v4 layout and utilities on the Reforma starter. Use when writing or - editing className, spacing, or size — not when adding a shadcn primitive - (Skill shadcn). ---- - -# Tailwind - -Starter is Tailwind **4** (`@tailwindcss/postcss`, no `tailwind.config.*`). -`cn` is `import { cn } from '@/lib/utils'` (`clsx` + `tailwind-merge`). Fonts: -**Skill** `fonts`. Color and collection CSS: **Skill** `tokens`. - -## Layout - -`flex` + `gap-*`, not `space-y-*` / `space-x-*`. Equal width and height → -`size-*`. Ellipsis → `truncate`. Conditionals → `cn()`. - -`className` on shadcn primitives is layout (`max-w-md`, `mx-auto`, `mt-4`). -Variants and semantic tokens change look. - -```tsx -import { cn } from "@/lib/utils"; - -
- -
; -``` diff --git a/marketplace/system/visuals/.reforma-plugin/plugin.json b/marketplace/system/visuals/.reforma-plugin/plugin.json deleted file mode 100644 index fcaf60d..0000000 --- a/marketplace/system/visuals/.reforma-plugin/plugin.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "visuals", - "description": "Type, color, marks, and icons for this product", - "system": true, - "logo": "assets/logo.svg", - "interface": { - "displayName": "Visuals", - "brandColor": "#000000", - "logoSmall": "assets/logo-small.svg" - }, - "logoSmall": "assets/logo-small.svg" -} diff --git a/marketplace/system/visuals/assets/logo-small.svg b/marketplace/system/visuals/assets/logo-small.svg deleted file mode 100644 index 040e561..0000000 --- a/marketplace/system/visuals/assets/logo-small.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/marketplace/system/visuals/assets/logo.svg b/marketplace/system/visuals/assets/logo.svg deleted file mode 100644 index 729d2a7..0000000 --- a/marketplace/system/visuals/assets/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/marketplace/system/visuals/skills/fonts/SKILL.md b/marketplace/system/visuals/skills/fonts/SKILL.md deleted file mode 100644 index 8d21e0d..0000000 --- a/marketplace/system/visuals/skills/fonts/SKILL.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -name: fonts -disable-user-invocation: true -description: >- - Wires project fonts. Use when adding, replacing, or removing a font, or when - a --font-* role must point at a different face. ---- - -# Fonts - -Import WOFF2/TTF with ImportAsset. Read `app/fonts/index.ts` first. - -Touch only `app/fonts/` and the active collection CSS -(`app/tokens/collections.json` → `active` → `file`). No `next/font` elsewhere, -no CDN ``, no `@import` for fonts. - -| Path | Role | -| -------------------------- | --------------------------------- | -| `app/fonts/index.ts` | Connected faces + `fontVariables` | -| `app/fonts/manifest.json` | Custom font library | -| `app/fonts/assets/*.woff2` | Custom files | -| active `col_*.css` | Role tokens only | - -## Face vs role - -A **face** is a `next/font` export. It owns a CSS variable -(`--font-sans`, `--font-playfair-display`, …). `next/font` sets that variable. -Do not assign a face variable in collection CSS. - -A **role** is what UI uses: `--font-body` / `--font-heading` / `--font-display` -(`font-body` / `font-heading` / `font-display`). Not `next/font` `className`. - -A connected face does nothing until a role (or other file) contains the -literal `var(--that-face-variable)`. Never `` `--font-${family}` ``, -`'--font-' + name`, or `import * as fonts`. - -If the collection has `--font-sans: var(--font-body)`, delete that line — it -overwrites the face. Point roles at the face: - -```css ---font-body: var(--font-sans); ---font-heading: var(--font-sans); ---font-display: var(--font-sans); -``` - -## `index.ts` - -Keep it static and literal. No helpers, spreads, template strings, `import *`. - -- Google: named imports from `next/font/google`. Family spaces → `_` - (`Playfair Display` → `Playfair_Display`). -- Custom files: `import localFont from 'next/font/local'`. -- One `export const` per face. Binding is camelCase (`playfairDisplay`). -- `export const fontVariables = [a.variable, b.variable].join(' ')` — every - connected face, in that form. -- Always `display: 'swap'`, `preload: false`. -- `subsets: ['latin']`. Add `'cyrillic'` or `'latin-ext'` when copy needs it. - -`weight`: omit it on variable Google families (Inter, Geist, Roboto Flex, …). -Every other Google family: `weight: ['400', '700']` (strings). Add more -weights only if you use them. If the dev overlay says a weight is missing, -drop that weight. If it says `weight` is required, add `['400', '700']`. - -New face: `Playfair Display` → import `Playfair_Display`, binding -`playfairDisplay`, variable `--font-playfair-display`. Two faces must not -share a `variable`. - -### Replace vs add - -**Replace** the font behind an existing role: swap the loader. Keep the same -`variable`. Rename the binding to the new family (`inter` → `roboto`) and -update `fontVariables`. Inter uses `--font-sans` — keep that variable when -replacing Inter. Do not write `export const inter = Roboto(…)`. - -**Add** a face: new binding, new `--font-`, append to `fontVariables`, -point a role at `var(--font-…)`: - -```css ---font-heading: var(--font-playfair-display); -``` - -Replace Inter with Roboto (keep `--font-sans`) and add Playfair: - -```ts -import { Roboto, Playfair_Display } from "next/font/google"; - -export const roboto = Roboto({ - subsets: ["latin"], - weight: ["400", "700"], - variable: "--font-sans", - display: "swap", - preload: false, -}); - -export const playfairDisplay = Playfair_Display({ - subsets: ["latin"], - weight: ["400", "700"], - variable: "--font-playfair-display", - display: "swap", - preload: false, -}); - -export const fontVariables = [roboto.variable, playfairDisplay.variable].join( - " ", -); -``` - -## Stop using vs delete - -**Stop using** a face: point the role at another connected face so that face's -`--font-…` string is gone from tokens. Do not remove the `index.ts` export — -unused connections are stripped from `index.ts` when the project is saved. -`manifest.json` and `assets/` stay. - -**Delete** a custom font (user asked to remove it from the project): drop the -`index.ts` export, the manifest row, and the WOFF2 together. Leave other -families. - -## Custom local - -Three path forms for the same file: - -| Where | Path | -| ----------- | -------------------------------------- | -| ImportAsset | `app/fonts/assets/brand-regular.woff2` | -| `manifest` | `brand-regular.woff2` (basename) | -| `localFont` | `./assets/brand-regular.woff2` | - -Basename: `[a-z0-9][a-z0-9._-]*\.woff2`. No spaces. Lowercase if the source -name has capitals. - -1. If `manifest.json` already lists that family, reuse its `binding` / - `variable` — only connect in `index.ts`. -2. Else `ImportAsset` with `destination: "filesystem"` and - `path: "app/fonts/assets/.woff2"`. -3. Add a manifest row and a `localFont` export. `binding` (the `export const` - name) and `variable` must be identical in both files. Each file needs - `style`: `"normal"` or `"italic"`. Variable font: one file, - `weight: "100 900"` in both the manifest and `src`. - -```json -{ - "version": 1, - "fonts": [ - { - "binding": "brand", - "family": "Brand", - "variable": "--font-brand", - "files": [ - { "path": "brand-regular.woff2", "weight": "400", "style": "normal" } - ] - } - ] -} -``` - -```ts -import localFont from "next/font/local"; - -export const brand = localFont({ - src: [ - { path: "./assets/brand-regular.woff2", weight: "400", style: "normal" }, - ], - variable: "--font-brand", - display: "swap", - preload: false, -}); -``` diff --git a/marketplace/system/visuals/skills/icons/SKILL.md b/marketplace/system/visuals/skills/icons/SKILL.md deleted file mode 100644 index 963c8ae..0000000 --- a/marketplace/system/visuals/skills/icons/SKILL.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: icons -disable-user-invocation: true -description: >- - Picks UI chrome glyphs from the project icon pack. Use when adding a - chevron, search, close, spinner, status, or other interface icon. ---- - -# Icons - -Chevrons, search, close, spinners, status, and other chrome: an **icon pack -already in the project**. Starter is Lucide — -`import { SearchIcon } from 'lucide-react'` (suffix `Icon`, same as -`components/ui`). Pack name: `components.json` → `iconLibrary`. A different -pack already in `package.json` — that import, not a second pack. - -A new pack only when the user named it → **InstallDependencies**. No inline SVG -for a glyph a pack already has. No icon CDNs. - -App mark, favicon, or a drawing that is not chrome: **Skill** `svg`. - -Done when the glyph is an import from that pack. diff --git a/marketplace/system/visuals/skills/svg/SKILL.md b/marketplace/system/visuals/skills/svg/SKILL.md deleted file mode 100644 index c9e3e79..0000000 --- a/marketplace/system/visuals/skills/svg/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: svg -description: >- - Crafts an app mark, favicon, or inline SVG illustration. Use when the user - wants a logo, favicon, or a custom SVG drawing. ---- - -# SVG - -UI chrome icons: **Skill** `icons`. - -## Mark - -Favicon, app symbol, or a drawing that is not chrome. Photo-like art: installed -Recraft / Quiver skill. - -Geometric `path` / `circle` / `rect`. `viewBox` set. `fill="currentColor"` (or -a token class). No `` — letters in the header are HTML + **Skill** -`fonts`. Color values: **Skill** `tokens`. - -`d` commands: **uppercase = absolute**, lowercase = relative. Each command -takes exactly those args. - -| Cmd | Job | Args | -| --- | ---------------- | --------------------------------- | -| `M` | Move | `x y` | -| `L` | Line | `x y` | -| `H` | Horizontal | `x` | -| `V` | Vertical | `y` | -| `C` | Cubic bézier | `x1 y1 x2 y2 x y` | -| `S` | Smooth cubic | `x2 y2 x y` | -| `Q` | Quadratic | `x1 y1 x y` | -| `T` | Smooth quadratic | `x y` | -| `A` | Arc | `rx ry x-rot large-arc sweep x y` | -| `Z` | Close | — | - -`A`: `large-arc` 0 = short, 1 = >180°. `sweep` 0 = CCW, 1 = CW. `C`: first -control = leave, second = arrive. - -Tab icon: `app/icon.svg`. In-app: `components/logo.tsx` with -`cn('size-8', className)`. - -```tsx -import { cn } from "@/lib/utils"; - -export function Logo({ className }: { className?: string }) { - return ( - - - - ); -} -``` - -Done when it reads at **16px** and at **512px**. - -## Motion - -Property animation (draw-on, SMIL, morph): [animate.md](animate.md). Page -choreography: installed **Skill** `motion` / `gsap`. SVG timeline editor: -installed **Skill** `svgator`. diff --git a/marketplace/system/visuals/skills/svg/animate.md b/marketplace/system/visuals/skills/svg/animate.md deleted file mode 100644 index 7a6a2fb..0000000 --- a/marketplace/system/visuals/skills/svg/animate.md +++ /dev/null @@ -1,112 +0,0 @@ -# Animate - -Mark motion lives on the SVG nodes. Page choreography: installed **Skill** `motion` / `gsap`. Timeline editor: installed **Skill** `svgator`. - -Inline in `components/logo.tsx` → CSS. `app/icon.svg` / `` / `background-image` → SMIL in the markup (CSS cannot reach). Rest pose is frame 0. - -`transform` and `opacity` composite. `d`, `r`, `cx`, `points` repaint — spare. `transform-origin: center` — SVG default is `0 0`. - -`@media (prefers-reduced-motion: reduce)` → no CSS animation. SMIL: omit ``. - -Done when the still frame is the logo, then the property the user named actually moves. - -## CSS properties - -These interpolate as CSS: `fill`, `stroke`, `opacity`, `transform`, `stroke-dasharray`, `stroke-dashoffset`. Color = `currentColor` or a token class. - -```css -.mark { - transform-origin: center; - animation: pulse 2s ease-in-out infinite; -} -@keyframes pulse { - 50% { - transform: scale(1.08); - opacity: 0.85; - } -} -``` - -**Draw-on.** `pathLength="1"` — do not guess dash length. - -```tsx - -``` - -```css -.draw { - stroke-dasharray: 1; - stroke-dashoffset: 1; - animation: draw 1.2s ease forwards; -} -@keyframes draw { - to { - stroke-dashoffset: 0; - } -} -``` - -Stagger siblings with `animation-delay`. Morph `d` in CSS only when both paths have the **same commands in the same order**: - -```css -path { - d: path("M 4 16 L 28 16"); - transition: d 0.3s ease; -} -path:hover { - d: path("M 8 8 L 24 24"); -} -``` - -## SMIL - -Child of the shape. `fill="freeze"` keeps the end state. - -```svg - - - -``` - -| Element | Job | -| ------------------------- | ------------------------------------------------------------ | -| `` | Any attr (`r`, `cx`, `d`, `stroke-dashoffset`, `stop-color`) | -| `` | `translate` / `scale` / `rotate` / `skewX` / `skewY` | -| `` | Follow a path; `rotate="auto"` = tangent | -| `` | Jump an attr at `begin` | - -Chain: `id` on the first, `begin="first.end"` or `begin="first.end + 0.3s"`. Also `begin="click"` / `begin="2s"`. - -Easing: `calcMode="spline"` + `keySplines="0.42 0 0.58 1"` (one spline per interval). - -Morph: identical command count and types (table in SKILL.md). Same letters, same argument counts, same order. - -```svg - - - -``` - -Motion path: - -```svg - - - - - - -``` diff --git a/marketplace/system/visuals/skills/tokens/SKILL.md b/marketplace/system/visuals/skills/tokens/SKILL.md deleted file mode 100644 index 831d0d1..0000000 --- a/marketplace/system/visuals/skills/tokens/SKILL.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: tokens -disable-user-invocation: true -description: >- - Wires project color and theme tokens. Use when adding or changing a color, - palette, dark mode, radius, or other --color-* / collection CSS. ---- - -# Tokens - -Read `app/tokens/collections.json` → `active` → `file`. That `col_*.css` is -the only file to edit for design values. - -| Path | Role | -| ------------------------- | ---------------------------------------------- | -| `app/tokens/tailwind.css` | Framework bootstrap. Do not add design colors. | -| `app/tokens/index.css` | Generated re-export. Do not edit. | -| `app/globals.css` | Imports the two sheets. No `@theme` here. | - -Light lives in `@theme`. Dark overrides in `.dark`. Pages use semantic -utilities (`bg-background`, `text-muted-foreground`) — they map from -`--color-*`. No raw hex in `className`. No `dark:bg-gray-950`. - -Canonical name is `--color-`. shadcn short aliases stay in lockstep: - -```css ---color-brand: oklch(0.55 0.12 250); ---brand: var(--color-brand); -``` - -New color: both `@theme` and `.dark` if it must switch. Match neighboring -`oklch(...)`. Font roles: **Skill** `fonts`. - -Done when the page uses the utility or `var(--color-*)` and the collection -file holds the value. diff --git a/marketplace/ui/aceternity/.reforma-plugin/plugin.json b/marketplace/ui/aceternity/.reforma-plugin/plugin.json index 8fd19a7..4ab2a3c 100644 --- a/marketplace/ui/aceternity/.reforma-plugin/plugin.json +++ b/marketplace/ui/aceternity/.reforma-plugin/plugin.json @@ -7,7 +7,6 @@ }, "homepage": "https://ui.aceternity.com", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/ui/coss/.reforma-plugin/plugin.json b/marketplace/ui/coss/.reforma-plugin/plugin.json index 6f07c9a..4f9278a 100644 --- a/marketplace/ui/coss/.reforma-plugin/plugin.json +++ b/marketplace/ui/coss/.reforma-plugin/plugin.json @@ -9,7 +9,6 @@ "repository": "https://github.com/cosscom/coss", "license": "MIT", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/ui/flexnative/.reforma-plugin/plugin.json b/marketplace/ui/flexnative/.reforma-plugin/plugin.json index aabd9ea..62c5027 100644 --- a/marketplace/ui/flexnative/.reforma-plugin/plugin.json +++ b/marketplace/ui/flexnative/.reforma-plugin/plugin.json @@ -9,7 +9,6 @@ "repository": "https://github.com/felipemenezes098/ui-flx", "license": "AGPL-3.0", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/ui/magic-ui/.reforma-plugin/plugin.json b/marketplace/ui/magic-ui/.reforma-plugin/plugin.json index bb7cb59..fd9f5fe 100644 --- a/marketplace/ui/magic-ui/.reforma-plugin/plugin.json +++ b/marketplace/ui/magic-ui/.reforma-plugin/plugin.json @@ -9,7 +9,6 @@ "repository": "https://github.com/magicuidesign/magicui", "license": "MIT", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/ui/tailark/.reforma-plugin/plugin.json b/marketplace/ui/tailark/.reforma-plugin/plugin.json index 7739fc3..e8f6708 100644 --- a/marketplace/ui/tailark/.reforma-plugin/plugin.json +++ b/marketplace/ui/tailark/.reforma-plugin/plugin.json @@ -7,7 +7,6 @@ }, "homepage": "https://tailark.com", "agent": { - "discover": true, "installApproval": false }, "interface": { diff --git a/marketplace/workspace/context7/.reforma-plugin/plugin.json b/marketplace/workspace/context7/.reforma-plugin/plugin.json index be58882..46d81e3 100644 --- a/marketplace/workspace/context7/.reforma-plugin/plugin.json +++ b/marketplace/workspace/context7/.reforma-plugin/plugin.json @@ -10,7 +10,6 @@ }, "mcpServers": "./mcp.json", "agent": { - "discover": true, "installApproval": false }, "logoSmall": "assets/logo-small.svg" diff --git a/marketplace/workspace/github-mcp/.reforma-plugin/plugin.json b/marketplace/workspace/github/.reforma-plugin/plugin.json similarity index 90% rename from marketplace/workspace/github-mcp/.reforma-plugin/plugin.json rename to marketplace/workspace/github/.reforma-plugin/plugin.json index f30bafd..9a273dc 100644 --- a/marketplace/workspace/github-mcp/.reforma-plugin/plugin.json +++ b/marketplace/workspace/github/.reforma-plugin/plugin.json @@ -7,9 +7,6 @@ }, "logo": "assets/logo.svg", "mcpServers": "./mcp.json", - "agent": { - "discover": true - }, "interface": { "displayName": "GitHub", "brandColor": "#FFFFFF", diff --git a/marketplace/workspace/github-mcp/assets/logo-small.svg b/marketplace/workspace/github/assets/logo-small.svg similarity index 100% rename from marketplace/workspace/github-mcp/assets/logo-small.svg rename to marketplace/workspace/github/assets/logo-small.svg diff --git a/marketplace/workspace/github-mcp/assets/logo.svg b/marketplace/workspace/github/assets/logo.svg similarity index 100% rename from marketplace/workspace/github-mcp/assets/logo.svg rename to marketplace/workspace/github/assets/logo.svg diff --git a/marketplace/workspace/github-mcp/mcp.json b/marketplace/workspace/github/mcp.json similarity index 80% rename from marketplace/workspace/github-mcp/mcp.json rename to marketplace/workspace/github/mcp.json index 26ea6a6..c062ca9 100644 --- a/marketplace/workspace/github-mcp/mcp.json +++ b/marketplace/workspace/github/mcp.json @@ -1,6 +1,6 @@ { "mcpServers": { - "github-mcp": { + "github": { "url": "https://api.githubcopilot.com/mcp/" } } diff --git a/scripts/pack/agent.test.ts b/scripts/pack/agent.test.ts new file mode 100644 index 0000000..d3e0fae --- /dev/null +++ b/scripts/pack/agent.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { foldMentionText, resolvePackedAgent, stampPackedAgent } from "./agent.ts"; +import type { MarketplaceListing } from "./shared.ts"; + +function listing( + overrides: Partial & Pick, +): MarketplaceListing { + return { + source: "./marketplace/demo", + category: "workspace", + ...overrides, + }; +} + +describe("foldMentionText", () => { + it("folds case, hyphens, and punctuation", () => { + expect(foldMentionText("Google-Drive!")).toBe("google drive"); + expect(foldMentionText("linear.app")).toBe("linear.app"); + }); +}); + +describe("resolvePackedAgent", () => { + it("defaults to folded id and displayName", () => { + expect( + resolvePackedAgent( + { interface: { displayName: "GitHub" } }, + listing({ name: "github" }), + ), + ).toEqual({ mentions: ["github"] }); + }); + + it("replaces defaults with an explicit mentions array", () => { + expect( + resolvePackedAgent( + { agent: { mentions: ["linear.app"] } }, + listing({ name: "linear", displayName: "Linear" }), + ), + ).toEqual({ mentions: ["linear.app"] }); + }); + + it("turns false or [] into no mentions", () => { + expect( + resolvePackedAgent({ agent: { mentions: false } }, listing({ name: "motion" })), + ).toBeUndefined(); + expect( + resolvePackedAgent({ agent: { mentions: [] } }, listing({ name: "motion" })), + ).toBeUndefined(); + }); + + it("listing.agent overlays plugin.json agent", () => { + expect( + resolvePackedAgent( + { agent: { mentions: ["linear"], installApproval: false } }, + listing({ + name: "linear", + agent: { mentions: ["linear.app"] }, + }), + ), + ).toEqual({ mentions: ["linear.app"], installApproval: false }); + }); + + it("keeps installApproval false and drops discover", () => { + expect( + resolvePackedAgent( + { agent: { discover: true, installApproval: false } }, + listing({ name: "coss", displayName: "coss" }), + ), + ).toEqual({ mentions: ["coss"], installApproval: false }); + }); +}); + +describe("stampPackedAgent", () => { + const dirs: string[] = []; + + afterEach(() => { + for (const dir of dirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("writes resolved agent onto plugin.json", () => { + const dir = mkdtempSync(join(tmpdir(), "agent-stamp-")); + const pluginDir = join(dir, "neon"); + + dirs.push(dir); + mkdirSync(join(pluginDir, ".reforma-plugin"), { recursive: true }); + writeFileSync( + join(pluginDir, ".reforma-plugin/plugin.json"), + `${JSON.stringify({ name: "neon", agent: { mentions: ["neon.tech"] } }, null, 4)}\n`, + ); + + stampPackedAgent(pluginDir, listing({ name: "neon" })); + + expect( + JSON.parse(readFileSync(join(pluginDir, ".reforma-plugin/plugin.json"), "utf8")), + ).toEqual({ + name: "neon", + agent: { mentions: ["neon.tech"] }, + }); + }); +}); diff --git a/scripts/pack/agent.ts b/scripts/pack/agent.ts new file mode 100644 index 0000000..4ba9bd7 --- /dev/null +++ b/scripts/pack/agent.ts @@ -0,0 +1,131 @@ +/** + * Resolve `plugin.json` `agent` at pack time: mentions defaults + fold, + * strip `discover`. Runtime only matches the packed array. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { findManifestPath, isRecord, type MarketplaceListing } from "./shared.ts"; + +/** Lowercase, hyphen → space, drop other punctuation except `.`. */ +export function foldMentionText(value: string): string { + return value + .toLowerCase() + .replaceAll("-", " ") + .replace(/[^\p{L}\p{N}.]+/gu, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function uniqueFolded(values: readonly string[]): string[] { + const out: string[] = []; + const seen = new Set(); + + for (const value of values) { + const folded = foldMentionText(value); + + if (!folded || seen.has(folded)) { + continue; + } + + seen.add(folded); + out.push(folded); + } + + return out; +} + +function displayNameForDefaults( + json: Record, + listing: MarketplaceListing, +): string { + const iface = isRecord(json.interface) ? json.interface : undefined; + const fromInterface = + typeof iface?.displayName === "string" ? iface.displayName.trim() : ""; + + if (fromInterface) { + return fromInterface; + } + + return listing.displayName?.trim() || listing.name; +} + +function readMentionsField(raw: unknown, label: string): string[] | false | undefined { + if (raw === undefined) { + return undefined; + } + + if (raw === false) { + return false; + } + + if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) { + throw new Error(`${label} must be a string[] or false`); + } + + return raw; +} + +function mergeAgent( + json: Record, + listing: MarketplaceListing, +): Record { + const base = isRecord(json.agent) ? { ...json.agent } : {}; + + if (listing.agent) { + Object.assign(base, listing.agent); + } + + return base; +} + +/** Packed `agent`: folded mentions (omit when empty) + `installApproval: false`. */ +export function resolvePackedAgent( + json: Record, + listing: MarketplaceListing, +): Record | undefined { + const merged = mergeAgent(json, listing); + const mentionsRaw = readMentionsField( + merged.mentions, + `${listing.name} agent.mentions`, + ); + const mentions = + mentionsRaw === false || Array.isArray(mentionsRaw) + ? uniqueFolded(mentionsRaw === false ? [] : mentionsRaw) + : uniqueFolded([listing.name, displayNameForDefaults(json, listing)]); + + const installApproval = merged.installApproval; + const agent: Record = { + ...(mentions.length > 0 ? { mentions } : {}), + ...(installApproval === false ? { installApproval: false } : {}), + }; + + return Object.keys(agent).length > 0 ? agent : undefined; +} + +/** Last pack stamp so overlay / category writes cannot skip mentions. */ +export function stampPackedAgent( + pluginDir: string, + listing: MarketplaceListing, +): void { + const manifestPath = findManifestPath(pluginDir); + + if (!manifestPath) { + throw new Error(`no plugin.json in ${pluginDir}`); + } + + const json = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown; + + if (!isRecord(json)) { + throw new Error(`invalid plugin.json ${manifestPath}`); + } + + const agent = resolvePackedAgent(json, listing); + + if (agent) { + json.agent = agent; + } + else { + delete json.agent; + } + + writeFileSync(manifestPath, `${JSON.stringify(json, null, 4)}\n`); +} diff --git a/scripts/pack/index.ts b/scripts/pack/index.ts index 46fd63e..cbe2a51 100644 --- a/scripts/pack/index.ts +++ b/scripts/pack/index.ts @@ -20,6 +20,7 @@ import { normalizePluginHooks } from "./hooks.ts"; import { normalizePluginLayout } from "./layout.ts"; import { normalizePluginLogos, applyCatalogOverlay } from "./logos.ts"; import { discoverMcpTools } from "./mcp-tools.ts"; +import { stampPackedAgent } from "./agent.ts"; import { OUT, ROOT, TAR, parseMarketplace, type MarketplaceListing } from "./shared.ts"; import { logCatalogSummary } from "./summary.ts"; import { normalizePluginTools } from "./tools.ts"; @@ -37,6 +38,7 @@ async function packPlugin(listing: MarketplaceListing): Promise { await normalizePluginLogos(dest); await discoverMcpTools(dest); stampPluginCategory(dest, listing.category); + stampPackedAgent(dest, listing); return; } @@ -57,6 +59,7 @@ async function packPlugin(listing: MarketplaceListing): Promise { await normalizePluginLogos(dest); await discoverMcpTools(dest); stampPluginCategory(dest, listing.category); + stampPackedAgent(dest, listing); } const raw = JSON.parse( diff --git a/scripts/pack/shared.test.ts b/scripts/pack/shared.test.ts index eb49873..cae5d9e 100644 --- a/scripts/pack/shared.test.ts +++ b/scripts/pack/shared.test.ts @@ -91,4 +91,36 @@ describe("parseMarketplace", () => { }, ]); }); + + it("keeps listing agent overlay off packed categories", () => { + const parsed = parseMarketplace({ + categories: [ + { + id: "workspace", + name: "Workspace", + plugins: [ + { + name: "linear", + source: "https://github.com/linear/cursor-plugin/tree/abc", + agent: { mentions: ["linear.app"] }, + }, + ], + }, + ], + }); + + expect(parsed.plugins[0]?.agent).toEqual({ mentions: ["linear.app"] }); + expect(parsed.categories).toEqual([ + { + id: "workspace", + name: "Workspace", + plugins: [ + { + name: "linear", + source: "https://github.com/linear/cursor-plugin/tree/abc", + }, + ], + }, + ]); + }); }); diff --git a/scripts/pack/shared.ts b/scripts/pack/shared.ts index 2ea46a6..a82cf15 100644 --- a/scripts/pack/shared.ts +++ b/scripts/pack/shared.ts @@ -42,6 +42,11 @@ export function sameRelPath(left: string, right: string): boolean { return normalize(left) === normalize(right); } +export type MarketplaceListingAgent = { + mentions?: string[] | false; + installApproval?: boolean; +}; + export type MarketplaceListing = { name: string; source: string; @@ -51,6 +56,7 @@ export type MarketplaceListing = { logo?: string; logoSmall?: string; brandColor?: string; + agent?: MarketplaceListingAgent; }; /** Nested `categories[].plugins` → flat listings. Shelf is the parent category. */ @@ -140,6 +146,7 @@ export function parseMarketplace(raw: unknown): { ...optionalListingField(plugin, "logo"), ...optionalListingField(plugin, "logoSmall"), ...optionalListingField(plugin, "brandColor"), + ...optionalListingAgent(plugin), }); } @@ -168,6 +175,22 @@ function optionalListingField( return { [key]: value.trim() }; } +function optionalListingAgent( + plugin: Record, +): { agent?: MarketplaceListing["agent"] } { + const value = plugin.agent; + + if (value === undefined) { + return {}; + } + + if (!isRecord(value)) { + throw new Error(`marketplace.json plugin ${plugin.name} agent must be an object`); + } + + return { agent: value }; +} + export function asPathList(raw: unknown, label: string): string[] | undefined { if (raw === undefined) { return undefined;