@folpe/loom 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -16
- package/data/agents/backend/AGENT.md +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -15
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +37 -0
- package/data/presets/chrome-extension.yaml +36 -0
- package/data/presets/cli-tool.yaml +31 -0
- package/data/presets/e-commerce.yaml +49 -0
- package/data/presets/expo-mobile.yaml +41 -0
- package/data/presets/fullstack-auth.yaml +45 -0
- package/data/presets/landing-page.yaml +38 -0
- package/data/presets/mvp-lean.yaml +35 -0
- package/data/presets/saas-default.yaml +7 -11
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +149 -0
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +105 -0
- package/data/skills/cli-development/SKILL.md +147 -0
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +87 -0
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +106 -0
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +126 -0
- package/data/skills/stripe-integration/SKILL.md +96 -0
- package/data/skills/supabase-patterns/SKILL.md +110 -0
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +179 -0
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-development
|
|
3
|
+
description: "CLI tool development patterns for Node.js with Commander.js, terminal UX, error handling, and npm distribution. Use when building command-line tools, adding CLI commands, implementing terminal prompts, or bundling CLI binaries for distribution."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CLI Development Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Use `stdout` for data, `stderr` for logs** — never mix output channels.
|
|
11
|
+
- **Exit with proper codes** — `0` success, `1` runtime error, `2` usage error.
|
|
12
|
+
- **Never show raw stack traces** — log them with `--verbose` flag only.
|
|
13
|
+
- **Respect `NO_COLOR`** — check `process.env.NO_COLOR` before using colors.
|
|
14
|
+
- **Validate config with Zod** — fail fast with clear error on invalid config.
|
|
15
|
+
- **Confirm destructive actions** — always prompt before irreversible operations.
|
|
16
|
+
|
|
17
|
+
## Project Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
index.ts # Entry point — program definition and parse()
|
|
22
|
+
commands/ # One file per command/subcommand
|
|
23
|
+
init.ts
|
|
24
|
+
build.ts
|
|
25
|
+
list.ts
|
|
26
|
+
lib/ # Shared utilities
|
|
27
|
+
config.ts # Config loading and validation
|
|
28
|
+
output.ts # Formatting and printing helpers
|
|
29
|
+
errors.ts # Custom error classes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commander.js Setup
|
|
33
|
+
|
|
34
|
+
- Define the program with metadata from `package.json`:
|
|
35
|
+
```ts
|
|
36
|
+
const program = new Command()
|
|
37
|
+
.name('mytool')
|
|
38
|
+
.description('What this tool does')
|
|
39
|
+
.version(version)
|
|
40
|
+
```
|
|
41
|
+
- One file per subcommand — register with `program.addCommand()`.
|
|
42
|
+
- Use `.argument()` for required positional args, `.option()` for flags:
|
|
43
|
+
```ts
|
|
44
|
+
program
|
|
45
|
+
.command('init')
|
|
46
|
+
.description('Initialize a new project')
|
|
47
|
+
.argument('<name>', 'project name')
|
|
48
|
+
.option('-t, --template <template>', 'template to use', 'default')
|
|
49
|
+
.action(async (name, options) => { /* ... */ })
|
|
50
|
+
```
|
|
51
|
+
- Always provide `--help` descriptions for all arguments and options.
|
|
52
|
+
|
|
53
|
+
## Exit Codes
|
|
54
|
+
|
|
55
|
+
- `0` — success
|
|
56
|
+
- `1` — general error (runtime failure, unhandled exception)
|
|
57
|
+
- `2` — usage error (invalid arguments, missing required flags)
|
|
58
|
+
- Exit explicitly: `process.exit(code)` after cleanup.
|
|
59
|
+
- Catch unhandled errors at the top level:
|
|
60
|
+
```ts
|
|
61
|
+
program.parseAsync().catch((error) => {
|
|
62
|
+
console.error(error.message)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Output Conventions
|
|
68
|
+
|
|
69
|
+
- Use `stdout` for actual output (data, results, formatted tables).
|
|
70
|
+
- Use `stderr` for progress, logging, warnings, and errors.
|
|
71
|
+
- Support `--json` flag for machine-readable output on data commands.
|
|
72
|
+
- Support `--quiet` or `--silent` flag to suppress non-essential output.
|
|
73
|
+
- Use colors sparingly — respect `NO_COLOR` environment variable:
|
|
74
|
+
```ts
|
|
75
|
+
const useColor = !process.env.NO_COLOR && process.stdout.isTTY
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Terminal UX
|
|
79
|
+
|
|
80
|
+
- Show a spinner for long operations (use `ora` or `nanospinner`).
|
|
81
|
+
- Use progress bars for multi-step or percentage-based operations.
|
|
82
|
+
- Confirm destructive actions with a prompt (use `@inquirer/prompts` or `@clack/prompts`):
|
|
83
|
+
```ts
|
|
84
|
+
const confirmed = await confirm({ message: 'Delete all files?' })
|
|
85
|
+
if (!confirmed) process.exit(0)
|
|
86
|
+
```
|
|
87
|
+
- Use tables for structured data display (use `cli-table3` or `columnify`).
|
|
88
|
+
- Truncate long output with `--limit` option or pipe to `less`.
|
|
89
|
+
|
|
90
|
+
## Input
|
|
91
|
+
|
|
92
|
+
- Accept input from stdin for piping:
|
|
93
|
+
```ts
|
|
94
|
+
if (!process.stdin.isTTY) {
|
|
95
|
+
const input = await readStdin()
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
- Support both `--flag value` and `--flag=value` syntax (Commander.js handles this).
|
|
99
|
+
- Use environment variables as fallback for configuration: `MYTOOL_TOKEN`, `MYTOOL_CONFIG`.
|
|
100
|
+
|
|
101
|
+
## Error Messages
|
|
102
|
+
|
|
103
|
+
- Include what went wrong, why, and how to fix it:
|
|
104
|
+
```
|
|
105
|
+
Error: Config file not found at ./config.yaml
|
|
106
|
+
Run `mytool init` to create a default configuration.
|
|
107
|
+
```
|
|
108
|
+
- Use chalk/picocolors for error formatting:
|
|
109
|
+
- Red for errors
|
|
110
|
+
- Yellow for warnings
|
|
111
|
+
- Dim for secondary info
|
|
112
|
+
- Never show raw stack traces to users. Log them with `--verbose` flag.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
- Support config file (`.mytoolrc`, `mytool.config.ts`, or field in `package.json`).
|
|
117
|
+
- Use `cosmiconfig` or manual lookup for config file discovery.
|
|
118
|
+
- Validate config with Zod schema — fail fast with clear error on invalid config.
|
|
119
|
+
- Allow CLI flags to override config file values.
|
|
120
|
+
|
|
121
|
+
## Bundling & Distribution
|
|
122
|
+
|
|
123
|
+
- Bundle with `tsup` for a single distributable file:
|
|
124
|
+
```ts
|
|
125
|
+
// tsup.config.ts
|
|
126
|
+
export default defineConfig({
|
|
127
|
+
entry: ['src/index.ts'],
|
|
128
|
+
format: ['esm'],
|
|
129
|
+
target: 'node20',
|
|
130
|
+
clean: true,
|
|
131
|
+
banner: { js: '#!/usr/bin/env node' },
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
- Set `"bin"` in `package.json` pointing to the built file.
|
|
135
|
+
- Set `"type": "module"` for ESM.
|
|
136
|
+
- Test the built binary locally before publishing: `node dist/index.js`.
|
|
137
|
+
|
|
138
|
+
## Testing
|
|
139
|
+
|
|
140
|
+
- Test commands by invoking them programmatically, not by spawning processes:
|
|
141
|
+
```ts
|
|
142
|
+
import { program } from '../src/index'
|
|
143
|
+
program.parse(['node', 'test', 'init', 'my-project'])
|
|
144
|
+
```
|
|
145
|
+
- Test output by capturing stdout/stderr.
|
|
146
|
+
- Test error cases: missing args, invalid flags, missing config.
|
|
147
|
+
- Test stdin piping with mock readable streams.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drizzle-patterns
|
|
3
|
+
description: "Drizzle ORM patterns for schema design, migrations, transactions, and DAO functions. Use when creating database schemas, writing queries, implementing transactions, or working with PostgreSQL via Drizzle."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Drizzle ORM Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Infer types from schema** — never duplicate type definitions manually.
|
|
11
|
+
- **Use `limit()` on all queries** — never fetch unbounded result sets.
|
|
12
|
+
- **Select only needed columns** — avoid `select()` with no arguments on large tables.
|
|
13
|
+
- **Never nest transactions** — keep transactions short with no external API calls inside.
|
|
14
|
+
- **Never edit generated migration files** — review SQL before applying in production.
|
|
15
|
+
- **Add indexes** on columns used in `WHERE`, `JOIN`, and `ORDER BY`.
|
|
16
|
+
|
|
17
|
+
## Schema Design
|
|
18
|
+
|
|
19
|
+
- Define schemas in `src/schema/` with one file per entity.
|
|
20
|
+
- Export all tables from `src/schema/index.ts` barrel file.
|
|
21
|
+
- Use `pgTable` for table definitions with typed columns.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/schema/user.ts
|
|
25
|
+
import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
|
26
|
+
|
|
27
|
+
export const users = pgTable("users", {
|
|
28
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
29
|
+
email: text("email").notNull().unique(),
|
|
30
|
+
name: text("name").notNull(),
|
|
31
|
+
role: text("role", { enum: ["USER", "ADMIN"] }).notNull().default("USER"),
|
|
32
|
+
emailVerified: boolean("email_verified").notNull().default(false),
|
|
33
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
34
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Relations
|
|
39
|
+
|
|
40
|
+
- Define relations separately using `relations()`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { relations } from "drizzle-orm";
|
|
44
|
+
|
|
45
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
46
|
+
posts: many(posts),
|
|
47
|
+
organizations: many(organizationMembers),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
51
|
+
author: one(users, { fields: [posts.authorId], references: [users.id] }),
|
|
52
|
+
}));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Database Client
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// src/lib/db.ts
|
|
59
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
60
|
+
import * as schema from "@/schema";
|
|
61
|
+
|
|
62
|
+
export const db = drizzle(process.env.DATABASE_URL!, { schema });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Query Patterns
|
|
66
|
+
|
|
67
|
+
- Use the query builder for complex reads:
|
|
68
|
+
```ts
|
|
69
|
+
const result = await db.query.users.findMany({
|
|
70
|
+
where: eq(users.role, "ADMIN"),
|
|
71
|
+
with: { posts: true },
|
|
72
|
+
orderBy: [desc(users.createdAt)],
|
|
73
|
+
limit: 20,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Use `select()` for simple reads with specific columns:
|
|
78
|
+
```ts
|
|
79
|
+
const result = await db.select({
|
|
80
|
+
id: users.id,
|
|
81
|
+
name: users.name,
|
|
82
|
+
}).from(users).where(eq(users.role, "ADMIN"));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Transactions
|
|
86
|
+
|
|
87
|
+
- Wrap multi-table writes in transactions:
|
|
88
|
+
```ts
|
|
89
|
+
await db.transaction(async (tx) => {
|
|
90
|
+
const [org] = await tx.insert(organizations).values({ name }).returning();
|
|
91
|
+
await tx.insert(organizationMembers).values({
|
|
92
|
+
organizationId: org.id,
|
|
93
|
+
userId: currentUser.id,
|
|
94
|
+
role: "OWNER",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## DAO Pattern
|
|
100
|
+
|
|
101
|
+
- Create DAO functions in `src/dal/` for reusable queries:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// src/dal/user.dal.ts
|
|
105
|
+
export async function findUserById(id: string) {
|
|
106
|
+
return db.query.users.findFirst({
|
|
107
|
+
where: eq(users.id, id),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function findUsersByOrg(orgId: string) {
|
|
112
|
+
return db.select()
|
|
113
|
+
.from(users)
|
|
114
|
+
.innerJoin(orgMembers, eq(orgMembers.userId, users.id))
|
|
115
|
+
.where(eq(orgMembers.orgId, orgId));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function createUser(data: NewUser) {
|
|
119
|
+
const [user] = await db.insert(users).values(data).returning();
|
|
120
|
+
return user;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function updateUser(id: string, data: Partial<NewUser>) {
|
|
124
|
+
const [user] = await db.update(users)
|
|
125
|
+
.set({ ...data, updatedAt: new Date() })
|
|
126
|
+
.where(eq(users.id, id))
|
|
127
|
+
.returning();
|
|
128
|
+
return user;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function deleteUser(id: string) {
|
|
132
|
+
await db.delete(users).where(eq(users.id, id));
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Migrations
|
|
137
|
+
|
|
138
|
+
- Generate migrations with `npx drizzle-kit generate`.
|
|
139
|
+
- Apply with `npx drizzle-kit migrate`.
|
|
140
|
+
- Use `drizzle-kit push` only in development for quick iteration.
|
|
141
|
+
- Review generated SQL before applying in production.
|
|
142
|
+
|
|
143
|
+
## Type Inference
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
|
|
147
|
+
|
|
148
|
+
export type User = InferSelectModel<typeof users>;
|
|
149
|
+
export type NewUser = InferInsertModel<typeof users>;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Performance
|
|
153
|
+
|
|
154
|
+
- Add indexes on columns used in `WHERE`, `JOIN`, and `ORDER BY`:
|
|
155
|
+
```ts
|
|
156
|
+
import { index } from "drizzle-orm/pg-core";
|
|
157
|
+
|
|
158
|
+
export const posts = pgTable("posts", {
|
|
159
|
+
// columns...
|
|
160
|
+
}, (table) => [
|
|
161
|
+
index("posts_author_idx").on(table.authorId),
|
|
162
|
+
index("posts_created_at_idx").on(table.createdAt),
|
|
163
|
+
]);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- Use `prepare()` for frequently executed queries.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-validation
|
|
3
|
+
description: "Environment variable validation with Zod schemas and type-safe access. Use when configuring environment variables, adding new secrets, or setting up project configuration."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Environment Variable Validation
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Validate at startup** — crash early if env vars are missing/invalid.
|
|
11
|
+
- **Single source of truth** — `src/env.ts` is the only place to access env vars.
|
|
12
|
+
- **Never use `process.env` directly** — always import from `@/env`.
|
|
13
|
+
- **Type-safe prefixes** — validate format (e.g., `startsWith("sk_")` for Stripe keys).
|
|
14
|
+
- **Maintain `.env.example`** — document all required variables without values.
|
|
15
|
+
- **Never commit secrets** — `.env.local` and `.env.production` are gitignored.
|
|
16
|
+
|
|
17
|
+
## Schema Definition
|
|
18
|
+
|
|
19
|
+
Validate all environment variables at startup with a Zod schema:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/env.ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
|
|
25
|
+
const envSchema = z.object({
|
|
26
|
+
// Database
|
|
27
|
+
DATABASE_URL: z.string().url(),
|
|
28
|
+
|
|
29
|
+
// Auth
|
|
30
|
+
BETTER_AUTH_SECRET: z.string().min(32),
|
|
31
|
+
BETTER_AUTH_URL: z.string().url(),
|
|
32
|
+
|
|
33
|
+
// Email
|
|
34
|
+
RESEND_API_KEY: z.string().startsWith("re_"),
|
|
35
|
+
|
|
36
|
+
// Stripe
|
|
37
|
+
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
|
|
38
|
+
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
|
|
39
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
40
|
+
|
|
41
|
+
// App
|
|
42
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
43
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const env = envSchema.parse(process.env);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Always import from `@/env` — never access `process.env` directly:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Good
|
|
55
|
+
import { env } from "@/env";
|
|
56
|
+
const db = drizzle(env.DATABASE_URL);
|
|
57
|
+
|
|
58
|
+
// Bad — unvalidated, untyped
|
|
59
|
+
const db = drizzle(process.env.DATABASE_URL!);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Client-Side Variables
|
|
63
|
+
|
|
64
|
+
Only `NEXT_PUBLIC_` prefixed variables are available in the browser:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/env.ts
|
|
68
|
+
const clientSchema = z.object({
|
|
69
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
70
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const serverSchema = z.object({
|
|
74
|
+
DATABASE_URL: z.string().url(),
|
|
75
|
+
BETTER_AUTH_SECRET: z.string().min(32),
|
|
76
|
+
RESEND_API_KEY: z.string().startsWith("re_"),
|
|
77
|
+
// ...server-only vars
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Merge for full validation
|
|
81
|
+
const envSchema = serverSchema.merge(clientSchema);
|
|
82
|
+
|
|
83
|
+
export const env = envSchema.parse(process.env);
|
|
84
|
+
|
|
85
|
+
// Client-only export for safe use in client components
|
|
86
|
+
export const clientEnv = clientSchema.parse({
|
|
87
|
+
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
|
88
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## .env Files
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
.env # Shared defaults (committed)
|
|
96
|
+
.env.local # Local overrides (gitignored)
|
|
97
|
+
.env.development # Dev-specific
|
|
98
|
+
.env.production # Prod-specific (gitignored)
|
|
99
|
+
.env.test # Test-specific
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### .env.example
|
|
103
|
+
|
|
104
|
+
Maintain a `.env.example` with all required variables (no values):
|
|
105
|
+
|
|
106
|
+
```env
|
|
107
|
+
# Database
|
|
108
|
+
DATABASE_URL=
|
|
109
|
+
|
|
110
|
+
# Auth
|
|
111
|
+
BETTER_AUTH_SECRET=
|
|
112
|
+
BETTER_AUTH_URL=
|
|
113
|
+
|
|
114
|
+
# Email
|
|
115
|
+
RESEND_API_KEY=
|
|
116
|
+
|
|
117
|
+
# Stripe
|
|
118
|
+
STRIPE_SECRET_KEY=
|
|
119
|
+
STRIPE_WEBHOOK_SECRET=
|
|
120
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
121
|
+
|
|
122
|
+
# App
|
|
123
|
+
NEXT_PUBLIC_APP_URL=
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Startup Validation
|
|
127
|
+
|
|
128
|
+
The `env.ts` import triggers validation. Import it early in your app:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// src/app/layout.tsx
|
|
132
|
+
import "@/env"; // Validates env vars at startup
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If validation fails, the app crashes immediately with a clear error:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
ZodError: [
|
|
139
|
+
{ path: ["DATABASE_URL"], message: "Required" },
|
|
140
|
+
{ path: ["RESEND_API_KEY"], message: "Invalid" }
|
|
141
|
+
]
|
|
142
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: form-validation
|
|
3
|
+
description: "Zod dual validation patterns for client and server with react-hook-form and ShadCN Form. Use when building forms, implementing validation, or creating input schemas with i18n error messages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Form Validation Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **One Zod schema, two validations** — share between client and server.
|
|
11
|
+
- **Schema files in `src/schemas/`** — never inline schemas in components.
|
|
12
|
+
- **Use ShadCN Form components** — `FormField`, `FormMessage` for consistent UX.
|
|
13
|
+
- **Always show inline errors** — next to the field, not in toasts.
|
|
14
|
+
- **Disable submit while pending** — prevent double submissions.
|
|
15
|
+
- **Default values required** — every field needs a `defaultValues` entry in useForm.
|
|
16
|
+
|
|
17
|
+
## Dual Validation Strategy
|
|
18
|
+
|
|
19
|
+
Every form must validate on **both client and server**:
|
|
20
|
+
- **Client**: instant feedback, UX quality.
|
|
21
|
+
- **Server**: security, data integrity — never trust client validation alone.
|
|
22
|
+
|
|
23
|
+
Use a single Zod schema shared between client and server.
|
|
24
|
+
|
|
25
|
+
## Shared Schema Definition
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// src/schemas/user.schema.ts
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
export const createUserSchema = z.object({
|
|
32
|
+
name: z.string().min(2, "Name must be at least 2 characters").max(100),
|
|
33
|
+
email: z.string().email("Invalid email address"),
|
|
34
|
+
role: z.enum(["USER", "ADMIN"]).default("USER"),
|
|
35
|
+
bio: z.string().max(500).optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Client-Side Validation with react-hook-form
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
"use client";
|
|
45
|
+
import { useForm } from "react-hook-form";
|
|
46
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
47
|
+
import { createUserSchema, type CreateUserInput } from "@/schemas/user.schema";
|
|
48
|
+
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
|
|
49
|
+
import { Input } from "@/components/ui/input";
|
|
50
|
+
import { Button } from "@/components/ui/button";
|
|
51
|
+
|
|
52
|
+
export function CreateUserForm() {
|
|
53
|
+
const form = useForm<CreateUserInput>({
|
|
54
|
+
resolver: zodResolver(createUserSchema),
|
|
55
|
+
defaultValues: { name: "", email: "", role: "USER" },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Form {...form}>
|
|
60
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
61
|
+
<FormField
|
|
62
|
+
control={form.control}
|
|
63
|
+
name="name"
|
|
64
|
+
render={({ field }) => (
|
|
65
|
+
<FormItem>
|
|
66
|
+
<FormLabel>Name</FormLabel>
|
|
67
|
+
<FormControl><Input {...field} /></FormControl>
|
|
68
|
+
<FormMessage />
|
|
69
|
+
</FormItem>
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
{/* More fields... */}
|
|
73
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
74
|
+
Create
|
|
75
|
+
</Button>
|
|
76
|
+
</form>
|
|
77
|
+
</Form>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Server-Side Validation
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// src/actions/user.actions.ts
|
|
86
|
+
"use server";
|
|
87
|
+
import { createUserSchema } from "@/schemas/user.schema";
|
|
88
|
+
|
|
89
|
+
export async function createUser(input: unknown) {
|
|
90
|
+
const parsed = createUserSchema.safeParse(input);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
return { success: false, error: parsed.error.errors[0].message };
|
|
93
|
+
}
|
|
94
|
+
// proceed with validated data...
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## I18n Error Messages with Zod
|
|
99
|
+
|
|
100
|
+
For internationalized error messages, use a Zod error map:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// src/lib/zod-i18n.ts
|
|
104
|
+
import { z } from "zod";
|
|
105
|
+
import { getTranslations } from "next-intl/server";
|
|
106
|
+
|
|
107
|
+
export async function createI18nSchema() {
|
|
108
|
+
const t = await getTranslations("validation");
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
createUser: z.object({
|
|
112
|
+
name: z.string().min(2, t("name.min", { min: 2 })),
|
|
113
|
+
email: z.string().email(t("email.invalid")),
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or use a custom error map:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// src/lib/zod-error-map.ts
|
|
123
|
+
import { type ZodErrorMap, ZodIssueCode } from "zod";
|
|
124
|
+
|
|
125
|
+
export function createZodErrorMap(t: (key: string, params?: Record<string, unknown>) => string): ZodErrorMap {
|
|
126
|
+
return (issue, ctx) => {
|
|
127
|
+
switch (issue.code) {
|
|
128
|
+
case ZodIssueCode.too_small:
|
|
129
|
+
return { message: t("too_small", { minimum: issue.minimum }) };
|
|
130
|
+
case ZodIssueCode.too_big:
|
|
131
|
+
return { message: t("too_big", { maximum: issue.maximum }) };
|
|
132
|
+
case ZodIssueCode.invalid_string:
|
|
133
|
+
if (issue.validation === "email") return { message: t("invalid_email") };
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
return { message: ctx.defaultError };
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Multi-Card Form Layout
|
|
142
|
+
|
|
143
|
+
For complex forms, split into logical sections using Cards:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
147
|
+
<Card>
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<CardTitle>Personal Information</CardTitle>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="grid gap-4 md:grid-cols-2">
|
|
152
|
+
{/* Name, Email fields */}
|
|
153
|
+
</CardContent>
|
|
154
|
+
</Card>
|
|
155
|
+
|
|
156
|
+
<Card>
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<CardTitle>Preferences</CardTitle>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<CardContent className="space-y-4">
|
|
161
|
+
{/* Role, Bio fields */}
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
|
|
165
|
+
<div className="flex justify-end">
|
|
166
|
+
<Button type="submit">Create User</Button>
|
|
167
|
+
</div>
|
|
168
|
+
</form>
|
|
169
|
+
```
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hero-copywriting
|
|
3
|
-
description: "Guidelines for writing high-converting hero sections. Use when creating landing pages, marketing
|
|
4
|
-
allowed-tools: "Read, Write, Edit"
|
|
3
|
+
description: "Guidelines for writing high-converting hero sections with headlines, CTAs, and social proof. Use when creating landing pages, writing marketing copy, designing hero sections, or building any page that needs a compelling above-the-fold area."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Hero Section Copywriting
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Lead with the outcome, not the feature** — what does the user GET?
|
|
11
|
+
- **Headlines must be 8-12 words maximum** — short, punchy, outcome-focused.
|
|
12
|
+
- **CTA must use action verbs** — "Start", "Get", "Try", "Launch", "Build".
|
|
13
|
+
- **Always include social proof** — logos, metrics, ratings, or testimonials.
|
|
14
|
+
- **Be specific over generic** — "Save 5 hours a week" beats "Save time".
|
|
15
|
+
- **Speak to the reader directly** — use "you" and "your".
|
|
16
|
+
|
|
9
17
|
## Headline Rules
|
|
10
18
|
|
|
11
|
-
-
|
|
19
|
+
- Lead with the outcome, not the feature. What does the user GET?
|
|
12
20
|
- Bad: "AI-powered project management"
|
|
13
21
|
- Good: "Ship projects 3x faster with your AI co-pilot"
|
|
14
22
|
- Keep headlines to **8-12 words maximum**.
|
|
@@ -41,7 +49,7 @@ allowed-tools: "Read, Write, Edit"
|
|
|
41
49
|
|
|
42
50
|
## Visual Guidelines
|
|
43
51
|
|
|
44
|
-
- Hero should occupy the full viewport height on desktop (`min-h-
|
|
52
|
+
- Hero should occupy the full viewport height on desktop (`min-h-dvh` or `min-h-[80vh]`).
|
|
45
53
|
- Text should be left-aligned or center-aligned. Never justify.
|
|
46
54
|
- Maximum content width: `max-w-2xl` for centered, `max-w-xl` for left-aligned.
|
|
47
55
|
- Generous whitespace. Don't crowd the hero.
|