AgentSkillsCN

Feature Scaffold

功能框架

SKILL.md

Feature Scaffold Skill

Generate Complete Features: Create entire feature folders with all files in the correct structure.


🎯 Purpose

When user says: "Create a [feature] feature"

This skill generates:

  • Page components
  • API routes/Server Actions
  • Validation schemas
  • Types
  • Tests (optional)

All following consistent project structure.


📁 Feature Structure

code
src/
├── app/
│   └── [feature]/
│       ├── page.tsx           # List page
│       ├── loading.tsx        # Loading state
│       ├── error.tsx          # Error boundary
│       ├── new/
│       │   └── page.tsx       # Create page
│       ├── [id]/
│       │   ├── page.tsx       # Detail page
│       │   └── edit/
│       │       └── page.tsx   # Edit page
│       └── _components/
│           ├── list.tsx       # List component
│           ├── form.tsx       # Create/Edit form
│           ├── card.tsx       # Card component
│           └── delete-button.tsx
├── lib/
│   ├── actions/
│   │   └── [feature].ts       # Server Actions
│   └── validations/
│       └── [feature].ts       # Zod schemas
└── types/
    └── [feature].ts           # TypeScript types

🚀 Step 1: Types

typescript
// types/[feature].ts
export interface [Feature] {
  id: string
  // Add fields
  createdAt: Date
  updatedAt: Date
}

export type Create[Feature]Input = Omit<[Feature], 'id' | 'createdAt' | 'updatedAt'>
export type Update[Feature]Input = Partial<Create[Feature]Input>

🚀 Step 2: Validation

typescript
// lib/validations/[feature].ts
import { z } from "zod"

export const create[Feature]Schema = z.object({
  name: z.string().min(2).max(100),
  // Add fields
})

export const update[Feature]Schema = create[Feature]Schema.partial()

export type Create[Feature]Input = z.infer<typeof create[Feature]Schema>

🚀 Step 3: Server Actions

typescript
// lib/actions/[feature].ts
"use server"
import { auth } from "@/auth"
import { db } from "@/lib/db"
import { create[Feature]Schema } from "@/lib/validations/[feature]"
import { revalidatePath } from "next/cache"

export async function get[Features]() {
  const session = await auth()
  if (!session?.user) throw new Error("Unauthorized")
  
  return db.[feature].findMany({
    where: { userId: session.user.id },
    orderBy: { createdAt: "desc" },
  })
}

export async function get[Feature](id: string) {
  const session = await auth()
  if (!session?.user) throw new Error("Unauthorized")
  
  return db.[feature].findFirst({
    where: { id, userId: session.user.id },
  })
}

export async function create[Feature](formData: FormData) {
  const session = await auth()
  if (!session?.user) throw new Error("Unauthorized")
  
  const data = create[Feature]Schema.parse({
    name: formData.get("name"),
  })
  
  await db.[feature].create({
    data: { ...data, userId: session.user.id },
  })
  
  revalidatePath("/[features]")
  return { success: true }
}

export async function update[Feature](id: string, formData: FormData) {
  const session = await auth()
  if (!session?.user) throw new Error("Unauthorized")
  
  const existing = await db.[feature].findFirst({
    where: { id, userId: session.user.id },
  })
  if (!existing) throw new Error("Not found")
  
  const data = create[Feature]Schema.partial().parse({
    name: formData.get("name") || undefined,
  })
  
  await db.[feature].update({ where: { id }, data })
  
  revalidatePath("/[features]")
  return { success: true }
}

export async function delete[Feature](id: string) {
  const session = await auth()
  if (!session?.user) throw new Error("Unauthorized")
  
  await db.[feature].deleteMany({
    where: { id, userId: session.user.id },
  })
  
  revalidatePath("/[features]")
  return { success: true }
}

🚀 Step 4: List Page

tsx
// app/[features]/page.tsx
import { get[Features] } from "@/lib/actions/[feature]"
import { [Feature]List } from "./_components/list"
import { Button } from "@/components/ui/button"
import Link from "next/link"

export default async function [Features]Page() {
  const items = await get[Features]()
  
  return (
    <div className="container py-8">
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold">[Features]</h1>
        <Button asChild>
          <Link href="/[features]/new">Create New</Link>
        </Button>
      </div>
      <[Feature]List items={items} />
    </div>
  )
}

🚀 Step 5: Loading & Error

tsx
// app/[features]/loading.tsx
import { Skeleton } from "@/components/ui/skeleton"

export default function Loading() {
  return (
    <div className="container py-8">
      <Skeleton className="h-8 w-48 mb-6" />
      <div className="space-y-4">
        {[...Array(5)].map((_, i) => (
          <Skeleton key={i} className="h-24 w-full" />
        ))}
      </div>
    </div>
  )
}

// app/[features]/error.tsx
"use client"

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="container py-8 text-center">
      <h2 className="text-xl font-bold mb-4">Something went wrong</h2>
      <p className="text-muted-foreground mb-4">{error.message}</p>
      <Button onClick={reset}>Try again</Button>
    </div>
  )
}

🚀 Step 6: Components

tsx
// app/[features]/_components/list.tsx
"use client"
import { [Feature] } from "@/types/[feature]"
import { [Feature]Card } from "./card"

export function [Feature]List({ items }: { items: [Feature][] }) {
  if (items.length === 0) {
    return (
      <div className="text-center py-12 text-muted-foreground">
        No items yet. Create your first one!
      </div>
    )
  }
  
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {items.map((item) => (
        <[Feature]Card key={item.id} item={item} />
      ))}
    </div>
  )
}

// app/[features]/_components/card.tsx
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import Link from "next/link"
import { DeleteButton } from "./delete-button"

export function [Feature]Card({ item }: { item: [Feature] }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{item.name}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Content */}
      </CardContent>
      <CardFooter className="flex gap-2">
        <Button variant="outline" size="sm" asChild>
          <Link href={`/[features]/${item.id}/edit`}>Edit</Link>
        </Button>
        <DeleteButton id={item.id} />
      </CardFooter>
    </Card>
  )
}

// app/[features]/_components/delete-button.tsx
"use client"
import { delete[Feature] } from "@/lib/actions/[feature]"
import { Button } from "@/components/ui/button"
import { useTransition } from "react"

export function DeleteButton({ id }: { id: string }) {
  const [isPending, startTransition] = useTransition()
  
  const handleDelete = () => {
    if (!confirm("Delete this item?")) return
    startTransition(() => delete[Feature](id))
  }
  
  return (
    <Button
      variant="destructive"
      size="sm"
      onClick={handleDelete}
      disabled={isPending}
    >
      {isPending ? "..." : "Delete"}
    </Button>
  )
}

📋 Scaffold Checklist

After generating:

  • Types created
  • Validation schema created
  • Server actions created
  • List page created
  • Create page created
  • Edit page created
  • Detail page (if needed)
  • Loading state
  • Error boundary
  • All components created
  • Prisma schema updated
  • Migration run

🔄 Token Replacements

Replace these when using:

  • [Feature]Project, Task (PascalCase singular)
  • [feature]project, task (camelCase singular)
  • [Features]Projects, Tasks (PascalCase plural)
  • [features]projects, tasks (lowercase plural)

Scaffold. Customize. Ship!