diff --git a/apps/cursor/package.json b/apps/cursor/package.json
index 4514fc09..811e9f34 100644
--- a/apps/cursor/package.json
+++ b/apps/cursor/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"@cursor/sdk": "^1.0.12",
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
diff --git a/apps/cursor/src/actions/delete-plugin.ts b/apps/cursor/src/actions/delete-plugin.ts
new file mode 100644
index 00000000..59c94ddb
--- /dev/null
+++ b/apps/cursor/src/actions/delete-plugin.ts
@@ -0,0 +1,48 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { z } from "zod";
+import { createClient } from "@/utils/supabase/server";
+import { ActionError, authActionClient } from "./safe-action";
+
+export const deletePluginAction = authActionClient
+ .metadata({
+ actionName: "delete-plugin",
+ })
+ .schema(
+ z.object({
+ id: z.string().uuid(),
+ }),
+ )
+ .action(async ({ parsedInput: { id }, ctx: { userId } }) => {
+ const supabase = await createClient();
+
+ const { data: existing, error: fetchError } = await supabase
+ .from("plugins")
+ .select("id, owner_id, slug, active")
+ .eq("id", id)
+ .single();
+
+ if (fetchError || !existing) {
+ throw new ActionError("Plugin not found.");
+ }
+
+ if (existing.owner_id !== userId) {
+ throw new ActionError("You do not have permission to delete this plugin.");
+ }
+
+ const { error } = await supabase
+ .from("plugins")
+ .delete()
+ .eq("id", id)
+ .eq("owner_id", userId);
+
+ if (error) {
+ throw new ActionError(`Failed to delete plugin: ${error.message}`);
+ }
+
+ revalidatePath("/");
+ revalidatePath("/admin/plugins");
+
+ return { slug: existing.slug };
+ });
diff --git a/apps/cursor/src/actions/toggle-plugin-listing.ts b/apps/cursor/src/actions/toggle-plugin-listing.ts
new file mode 100644
index 00000000..77e6ac12
--- /dev/null
+++ b/apps/cursor/src/actions/toggle-plugin-listing.ts
@@ -0,0 +1,57 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { z } from "zod";
+import { createClient } from "@/utils/supabase/server";
+import { ActionError, authActionClient } from "./safe-action";
+
+export const togglePluginListingAction = authActionClient
+ .metadata({
+ actionName: "toggle-plugin-listing",
+ })
+ .schema(
+ z.object({
+ id: z.string().uuid(),
+ active: z.boolean(),
+ }),
+ )
+ .action(async ({ parsedInput: { id, active }, ctx: { userId } }) => {
+ const supabase = await createClient();
+
+ const { data: existing, error: fetchError } = await supabase
+ .from("plugins")
+ .select("id, owner_id, slug, permanently_blocked")
+ .eq("id", id)
+ .single();
+
+ if (fetchError || !existing) {
+ throw new ActionError("Plugin not found.");
+ }
+
+ if (existing.owner_id !== userId) {
+ throw new ActionError("You do not have permission to update this plugin.");
+ }
+
+ if (active && existing.permanently_blocked) {
+ throw new ActionError(
+ "This plugin cannot be published. Contact support if you believe this is a mistake.",
+ );
+ }
+
+ const { data, error } = await supabase
+ .from("plugins")
+ .update({ active })
+ .eq("id", id)
+ .eq("owner_id", userId)
+ .select("slug")
+ .single();
+
+ if (error) {
+ throw new ActionError(error.message);
+ }
+
+ revalidatePath("/");
+ revalidatePath(`/plugins/${data.slug}`);
+
+ return data;
+ });
diff --git a/apps/cursor/src/app/plugins/[slug]/edit/page.tsx b/apps/cursor/src/app/plugins/[slug]/edit/page.tsx
index 89db34f4..be2be469 100644
--- a/apps/cursor/src/app/plugins/[slug]/edit/page.tsx
+++ b/apps/cursor/src/app/plugins/[slug]/edit/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { EditPluginForm } from "@/components/forms/edit-plugin-form";
+import { PluginOwnerMenu } from "@/components/plugins/plugin-owner-menu";
import { Login } from "@/components/login";
import { getPluginBySlug } from "@/data/queries";
import { getSession } from "@/utils/supabase/auth";
@@ -41,8 +42,9 @@ export default async function Page({ params }: { params: Params }) {
return (
-
+
diff --git a/apps/cursor/src/components/plugins/plugin-card.tsx b/apps/cursor/src/components/plugins/plugin-card.tsx
index e24ad484..17fafb13 100644
--- a/apps/cursor/src/components/plugins/plugin-card.tsx
+++ b/apps/cursor/src/components/plugins/plugin-card.tsx
@@ -16,6 +16,7 @@ export type PluginCardData = {
keywords?: string[];
installCount?: number;
verified?: boolean;
+ unpublished?: boolean;
href: string;
};
@@ -57,6 +58,11 @@ export function PluginCard({ plugin }: { plugin: PluginCardData }) {
{plugin.name}
{plugin.verified && }
+ {plugin.unpublished && (
+
+ Unpublished
+
+ )}
diff --git a/apps/cursor/src/components/plugins/plugin-detail.tsx b/apps/cursor/src/components/plugins/plugin-detail.tsx
index 36863d53..e882be7f 100644
--- a/apps/cursor/src/components/plugins/plugin-detail.tsx
+++ b/apps/cursor/src/components/plugins/plugin-detail.tsx
@@ -6,7 +6,6 @@ import {
Copy,
Download,
Loader2,
- Pencil,
ShieldAlert,
} from "lucide-react";
import Image from "next/image";
@@ -23,6 +22,7 @@ import { createClient } from "@/utils/supabase/client";
import { PluginIconFallback } from "./plugin-icon";
import { StarButton } from "./star-button";
import { VerifiedBadge } from "./verified-badge";
+import { PluginOwnerMenu } from "./plugin-owner-menu";
import { VerifyControls } from "./verify-controls";
function isValidImageUrl(url: string | null): url is string {
@@ -255,6 +255,13 @@ export function PluginDetailView({ plugin }: { plugin: PluginRow }) {
+ {isOwner && !plugin.active && (
+
+
+ This plugin is unpublished and hidden from the directory.
+
+
+ )}
@@ -267,19 +274,11 @@ export function PluginDetailView({ plugin }: { plugin: PluginRow }) {
+
{formatCount(installCount)}
- {isOwner && (
-
-
- Edit
-
- )}
{
+ const supabase = createClient();
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ const userId = session?.user.id ?? null;
+ setIsOwner(!!userId && plugin.owner_id === userId);
+ });
+ }, [plugin.owner_id]);
+
+ useEffect(() => {
+ setActive(plugin.active);
+ }, [plugin.active]);
+
+ const requestPending = !!plugin.verification_requested_at;
+
+ const { execute: requestVerification, isExecuting: isRequesting } = useAction(
+ requestPluginVerificationAction,
+ {
+ onSuccess: () => {
+ toast.success("Verification requested. We'll review it shortly.");
+ router.refresh();
+ },
+ onError: ({ error }) => {
+ toast.error(
+ error.serverError ?? "Failed to submit verification request.",
+ );
+ },
+ },
+ );
+
+ const { execute: toggleListing, isExecuting: isToggling } = useAction(
+ togglePluginListingAction,
+ {
+ onSuccess: ({ input }) => {
+ setActive(input.active);
+ toast.success(
+ input.active ? "Plugin is now published." : "Plugin unpublished.",
+ );
+ setConfirmUnpublish(false);
+ router.refresh();
+ },
+ onError: ({ error }) => {
+ toast.error(error.serverError ?? "Failed to update listing status.");
+ },
+ },
+ );
+
+ const { execute: deletePlugin, isExecuting: isDeleting } = useAction(
+ deletePluginAction,
+ {
+ onSuccess: () => {
+ toast.success("Plugin deleted.");
+ setConfirmDelete(false);
+ router.push("/");
+ },
+ onError: ({ error }) => {
+ toast.error(error.serverError ?? "Failed to delete plugin.");
+ },
+ },
+ );
+
+ if (!isOwner) {
+ return null;
+ }
+
+ const busy = isRequesting || isToggling || isDeleting;
+ const canVerify = !plugin.verified && !requestPending;
+ const canPublish = !active && !plugin.permanently_blocked;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+ {canVerify ? (
+ requestVerification({ pluginId: plugin.id })}
+ >
+
+ Submit for verification
+
+ ) : null}
+
+ {requestPending && !plugin.verified ? (
+
+
+ Verification requested
+
+ ) : null}
+
+ {active ? (
+ setConfirmUnpublish(true)}
+ >
+
+ Unpublish
+
+ ) : canPublish ? (
+ toggleListing({ id: plugin.id, active: true })}
+ >
+
+ Publish
+
+ ) : null}
+
+
+
+ setConfirmDelete(true)}
+ >
+
+ Delete
+
+
+
+
+
+
+
+
+
+ Delete plugin permanently?
+
+ “{plugin.name}” and all of its components will be
+ removed from the directory. This cannot be undone.
+ {active ? " The plugin is currently published." : null}
+
+
+
+ Cancel
+ {
+ e.preventDefault();
+ deletePlugin({ id: plugin.id });
+ }}
+ >
+ {isDeleting ? (
+
+ ) : null}
+ Delete
+
+
+
+
+ >
+ );
+}
diff --git a/apps/cursor/src/components/plugins/verify-controls.tsx b/apps/cursor/src/components/plugins/verify-controls.tsx
index 88ddf3c3..36a95ec6 100644
--- a/apps/cursor/src/components/plugins/verify-controls.tsx
+++ b/apps/cursor/src/components/plugins/verify-controls.tsx
@@ -4,7 +4,6 @@ import { BadgeCheck, BadgeMinus, Check, Loader2, X } from "lucide-react";
import { useAction } from "next-safe-action/hooks";
import { useEffect, useState } from "react";
import { toast } from "sonner";
-import { requestPluginVerificationAction } from "@/actions/request-plugin-verification";
import {
dismissVerificationRequestAction,
setPluginVerifiedAction,
@@ -19,34 +18,18 @@ type Props = {
};
export function VerifyControls({ plugin }: Props) {
- const [isOwner, setIsOwner] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const supabase = createClient();
supabase.auth.getSession().then(({ data: { session } }) => {
const userId = session?.user.id ?? null;
- setIsOwner(!!userId && plugin.owner_id === userId);
setIsAdmin(isAdminClient(userId));
});
- }, [plugin.owner_id]);
+ }, []);
const requestPending = !!plugin.verification_requested_at;
- const { execute: requestVerification, isExecuting: isRequesting } = useAction(
- requestPluginVerificationAction,
- {
- onSuccess: () => {
- toast.success("Verification requested. We'll review it shortly.");
- },
- onError: ({ error }) => {
- toast.error(
- error.serverError ?? "Failed to submit verification request.",
- );
- },
- },
- );
-
const { execute: setVerified, isExecuting: isSettingVerified } = useAction(
setPluginVerifiedAction,
{
@@ -73,88 +56,59 @@ export function VerifyControls({ plugin }: Props) {
},
);
- const busy = isRequesting || isSettingVerified || isDismissing;
-
- // Admin controls take priority — admins acting on their own plugin should
- // see the admin actions, not the owner submit button.
- if (isAdmin) {
- if (plugin.verified) {
- return (
-
- );
- }
+ const busy = isSettingVerified || isDismissing;
- if (requestPending) {
- return (
-
-
-
-
- );
- }
+ if (!isAdmin) {
+ return null;
+ }
+ if (plugin.verified) {
return (
);
}
- if (!isOwner || plugin.verified) {
- return null;
- }
-
if (requestPending) {
return (
-
-
- Verification requested
-
+
+
+
+
);
}
@@ -163,14 +117,14 @@ export function VerifyControls({ plugin }: Props) {
variant="outline"
size="sm"
disabled={busy}
- onClick={() => requestVerification({ pluginId: plugin.id })}
+ onClick={() => setVerified({ pluginId: plugin.id, verified: true })}
>
- {isRequesting ? (
+ {isSettingVerified ? (
) : (
)}
- Submit for verification
+ Mark verified
);
}
diff --git a/apps/cursor/src/components/profile/profile-plugins.tsx b/apps/cursor/src/components/profile/profile-plugins.tsx
index 09be3ede..4bcc3625 100644
--- a/apps/cursor/src/components/profile/profile-plugins.tsx
+++ b/apps/cursor/src/components/profile/profile-plugins.tsx
@@ -24,6 +24,8 @@ function toPluginCard(plugin: PluginRow): PluginCardData {
mcpCount: components.filter((c) => c.type === "mcp_server").length,
keywords: plugin.keywords,
installCount: plugin.install_count,
+ verified: plugin.verified,
+ unpublished: !plugin.active,
href: `/plugins/${plugin.slug}`,
};
}
@@ -35,7 +37,7 @@ export async function ProfilePlugins({
userId: string;
isOwner: boolean;
}) {
- const { data } = await getUserPlugins(userId);
+ const { data } = await getUserPlugins(userId, { includeInactive: isOwner });
const plugins = (data ?? []).map(toPluginCard);
if (!plugins?.length) {
diff --git a/apps/cursor/src/components/ui/alert-dialog.tsx b/apps/cursor/src/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..22823383
--- /dev/null
+++ b/apps/cursor/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import * as React from "react";
+
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+};
diff --git a/apps/cursor/src/components/ui/dropdown-menu.tsx b/apps/cursor/src/components/ui/dropdown-menu.tsx
index 9a940646..c099dbae 100644
--- a/apps/cursor/src/components/ui/dropdown-menu.tsx
+++ b/apps/cursor/src/components/ui/dropdown-menu.tsx
@@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<