From 3fe9a47453810470edbdae9d7c51bcd07f4e8b5a Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:56:45 +0800 Subject: [PATCH] Implement global skill enablement --- .../skills/_components/skills-library.tsx | 123 ++++++ app/(dashboard)/skills/layout.tsx | 14 + app/(dashboard)/skills/page.tsx | 29 ++ components/sidebar.tsx | 7 +- docs/prds/README.md | 2 + .../global-skill-enablement-control-flow.md | 325 ++++++++++++++++ docs/prds/uninstall-skill-control-flow.md | 363 ++++++++++++++++++ lib/actions/skill.ts | 21 + lib/data/user-skill.ts | 10 + lib/db.ts | 8 +- lib/events/database/databaseListener.ts | 10 + lib/events/sandbox/sandboxListener.ts | 10 + lib/jobs/project-task/executors/index.ts | 3 + .../project-task/executors/install-skill.ts | 118 ++++++ lib/jobs/project-task/projectTaskReconcile.ts | 80 +++- .../project/create-project-from-github.ts | 8 + .../commands/project/create-project.ts | 9 +- .../commands/skill/enable-global-skill.ts | 129 +++++++ lib/platform/control/commands/skill/index.ts | 1 + .../project-task/create-install-skill-task.ts | 47 +++ .../project/create-project-with-sandbox.ts | 20 +- lib/repo/project-task.ts | 46 +++ lib/repo/user-skill.ts | 33 ++ lib/skills/catalog.ts | 27 ++ lib/startup/index.ts | 55 +-- .../migration.sql | 31 ++ prisma/schema.prisma | 26 +- 27 files changed, 1515 insertions(+), 40 deletions(-) create mode 100644 app/(dashboard)/skills/_components/skills-library.tsx create mode 100644 app/(dashboard)/skills/layout.tsx create mode 100644 app/(dashboard)/skills/page.tsx create mode 100644 docs/prds/global-skill-enablement-control-flow.md create mode 100644 docs/prds/uninstall-skill-control-flow.md create mode 100644 lib/actions/skill.ts create mode 100644 lib/data/user-skill.ts create mode 100644 lib/jobs/project-task/executors/install-skill.ts create mode 100644 lib/platform/control/commands/skill/enable-global-skill.ts create mode 100644 lib/platform/control/commands/skill/index.ts create mode 100644 lib/platform/persistence/project-task/create-install-skill-task.ts create mode 100644 lib/repo/user-skill.ts create mode 100644 lib/skills/catalog.ts create mode 100644 prisma/migrations/20260317120000_user_skill_global_enablement/migration.sql diff --git a/app/(dashboard)/skills/_components/skills-library.tsx b/app/(dashboard)/skills/_components/skills-library.tsx new file mode 100644 index 0000000..d303f9c --- /dev/null +++ b/app/(dashboard)/skills/_components/skills-library.tsx @@ -0,0 +1,123 @@ +'use client' + +import { useState, useTransition } from 'react' +import { MdBolt, MdCheckCircle, MdOpenInNew } from 'react-icons/md' +import { useRouter } from 'next/navigation' +import { toast } from 'sonner' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { enableGlobalSkill } from '@/lib/actions/skill' +import { getSkillCatalog } from '@/lib/skills/catalog' + +type SkillsLibraryProps = { + enabledSkillIds: string[] +} + +export function SkillsLibrary({ enabledSkillIds }: SkillsLibraryProps) { + const router = useRouter() + const [pendingSkillId, setPendingSkillId] = useState(null) + const [isPending, startTransition] = useTransition() + const catalog = getSkillCatalog() + const enabledSkillSet = new Set(enabledSkillIds) + + const handleEnable = (skillId: string) => { + startTransition(async () => { + setPendingSkillId(skillId) + + const result = await enableGlobalSkill(skillId) + if (!result.success) { + toast.error(result.error) + setPendingSkillId(null) + return + } + + toast.success('Global skill enabled. Install tasks will fan out across your projects.') + router.refresh() + setPendingSkillId(null) + }) + } + + return ( +
+
+ + Global Skills + +
+

Skills

+

+ Enabling a skill here creates global desired state for the user. Existing projects get + `INSTALL_SKILL` tasks immediately, and new projects inherit the same skill when they + are created. +

+
+
+ +
+ {catalog.map((skill) => { + const isEnabled = enabledSkillSet.has(skill.skillId) + const isLoading = isPending && pendingSkillId === skill.skillId + + return ( + + +
+
+ +
+ + {isEnabled ? 'Enabled' : 'Available'} + +
+
+ {skill.name} + {skill.description} +
+
+ + +
+
+ + Install Command +
+ + {skill.installCommand} + +
+
+ + + + + + +
+ ) + })} +
+
+ ) +} diff --git a/app/(dashboard)/skills/layout.tsx b/app/(dashboard)/skills/layout.tsx new file mode 100644 index 0000000..bb6b758 --- /dev/null +++ b/app/(dashboard)/skills/layout.tsx @@ -0,0 +1,14 @@ +import { Sidebar } from '@/components/sidebar' + +export default function SkillsLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/app/(dashboard)/skills/page.tsx b/app/(dashboard)/skills/page.tsx new file mode 100644 index 0000000..cede1ac --- /dev/null +++ b/app/(dashboard)/skills/page.tsx @@ -0,0 +1,29 @@ +import { redirect } from 'next/navigation' + +import { auth } from '@/lib/auth' +import { getUserSkills } from '@/lib/data/user-skill' + +import { SkillsLibrary } from './_components/skills-library' + +export const metadata = { + title: 'Skills | Fulling', + description: 'Enable global skills that will be installed across your projects.', +} + +export default async function SkillsPage() { + const session = await auth() + + if (!session) { + redirect('/login') + } + + const userSkills = await getUserSkills(session.user.id) + + return ( +
+ skill.skillId)} + /> +
+ ) +} diff --git a/components/sidebar.tsx b/components/sidebar.tsx index 9bf08f4..e99d9bb 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -1,3 +1,5 @@ +'use client' + import { FaGithub } from 'react-icons/fa6' import { MdDashboardCustomize, @@ -10,6 +12,7 @@ import { } from 'react-icons/md' import Image from 'next/image' import Link from 'next/link' +import { usePathname } from 'next/navigation' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' @@ -61,6 +64,8 @@ function LogoSection() { } function NavMenu() { + const pathname = usePathname() + return (