nextjs-hackathon-stack 0.1.32 → 0.1.34

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/dist/index.js CHANGED
@@ -43,13 +43,25 @@ async function runCli(argProjectName, skipInstall) {
43
43
  }
44
44
  projectName = result;
45
45
  }
46
- return { projectName, skipInstall };
46
+ const templateResult = await p2.select({
47
+ message: "What would you like to start with?",
48
+ options: [
49
+ { value: "example", label: "Todo example app", hint: "Full working app with todos CRUD" },
50
+ { value: "empty", label: "Empty app", hint: "Auth + empty dashboard" }
51
+ ]
52
+ });
53
+ if (p2.isCancel(templateResult)) {
54
+ p2.cancel("Cancelled");
55
+ process.exit(0);
56
+ }
57
+ const template = templateResult;
58
+ return { projectName, skipInstall, template };
47
59
  }
48
60
 
49
61
  // src/scaffold.ts
50
62
  import { execSync } from "child_process";
51
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkSync, writeFileSync } from "fs";
52
- import { join, dirname } from "path";
63
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, symlinkSync, writeFileSync } from "fs";
64
+ import { join, dirname, resolve } from "path";
53
65
  import { fileURLToPath } from "url";
54
66
  import * as p3 from "@clack/prompts";
55
67
  import pc2 from "picocolors";
@@ -60,7 +72,7 @@ function getTemplateDir() {
60
72
  function processTemplate(content, vars) {
61
73
  return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
62
74
  }
63
- function copyDir(src, dest, vars) {
75
+ function copyDir(src, dest, vars, skip = /* @__PURE__ */ new Set(), templateRoot = src) {
64
76
  mkdirSync(dest, { recursive: true });
65
77
  for (const entry of readdirSync(src)) {
66
78
  const srcPath = join(src, entry);
@@ -70,11 +82,25 @@ function copyDir(src, dest, vars) {
70
82
  }
71
83
  const destPath = join(dest, destEntry);
72
84
  const stat = lstatSync(srcPath);
85
+ const relPath = srcPath.slice(templateRoot.length + 1).replace(/\\/g, "/");
86
+ if (skip.has(relPath)) continue;
73
87
  if (stat.isSymbolicLink()) {
74
88
  const target = readlinkSync(srcPath);
75
- symlinkSync(target, destPath);
89
+ const resolvedTarget = resolve(dirname(srcPath), target);
90
+ if (existsSync(resolvedTarget)) {
91
+ const resolvedStat = lstatSync(resolvedTarget);
92
+ if (resolvedStat.isFile()) {
93
+ cpSync(resolvedTarget, destPath);
94
+ } else if (resolvedStat.isDirectory()) {
95
+ cpSync(resolvedTarget, destPath, { recursive: true });
96
+ } else {
97
+ symlinkSync(target, destPath);
98
+ }
99
+ } else {
100
+ symlinkSync(target, destPath);
101
+ }
76
102
  } else if (stat.isDirectory()) {
77
- copyDir(srcPath, destPath, vars);
103
+ copyDir(srcPath, destPath, vars, skip, templateRoot);
78
104
  } else if (entry.endsWith(".tmpl")) {
79
105
  const content = readFileSync(srcPath, "utf-8");
80
106
  writeFileSync(destPath, processTemplate(content, vars));
@@ -83,7 +109,97 @@ function copyDir(src, dest, vars) {
83
109
  }
84
110
  }
85
111
  }
86
- async function scaffold(projectName, skipInstall) {
112
+ var EMPTY_APP_PAGE = `import { redirect } from "next/navigation";
113
+ import { logoutAction } from "@/features/auth/actions/logout.action";
114
+ import { createClient } from "@/shared/lib/supabase/server";
115
+
116
+ export default async function HomePage() {
117
+ const supabase = await createClient();
118
+ const { data: { user } } = await supabase.auth.getUser();
119
+ if (!user) redirect("/login");
120
+
121
+ return (
122
+ <main className="container mx-auto max-w-2xl p-8">
123
+ <div className="mb-6 flex items-center justify-between">
124
+ <h1 className="text-2xl font-bold">Dashboard</h1>
125
+ <form action={logoutAction}>
126
+ <button type="submit" className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10">
127
+ Cerrar sesi\xF3n
128
+ </button>
129
+ </form>
130
+ </div>
131
+ <p className="text-muted-foreground">\xA1Bienvenido! Empieza a construir tu aplicaci\xF3n.</p>
132
+ </main>
133
+ );
134
+ }
135
+ `;
136
+ var EMPTY_APP_SCHEMA = `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
137
+ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
138
+
139
+ export const profiles = pgTable("profiles", {
140
+ id: uuid("id").primaryKey(),
141
+ email: text("email").notNull().unique(),
142
+ createdAt: timestamp("created_at").notNull().defaultNow(),
143
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
144
+ });
145
+
146
+ export const insertProfileSchema = createInsertSchema(profiles);
147
+ export const selectProfileSchema = createSelectSchema(profiles);
148
+
149
+ export type InsertProfile = typeof profiles.$inferInsert;
150
+ export type SelectProfile = typeof profiles.$inferSelect;
151
+ `;
152
+ var EMPTY_APP_PAGE_TEST = `import { describe, it, expect, vi, beforeEach } from "vitest";
153
+
154
+ const mockGetUser = vi.fn();
155
+ const mockRedirect = vi.fn();
156
+
157
+ vi.mock("@/shared/lib/supabase/server", () => ({
158
+ createClient: vi.fn(() =>
159
+ Promise.resolve({ auth: { getUser: mockGetUser } })
160
+ ),
161
+ }));
162
+
163
+ vi.mock("next/navigation", () => ({
164
+ redirect: (url: string): unknown => mockRedirect(url),
165
+ }));
166
+
167
+ describe("HomePage (protected)", () => {
168
+ beforeEach(() => {
169
+ vi.clearAllMocks();
170
+ mockRedirect.mockImplementation(() => { throw new Error("NEXT_REDIRECT"); });
171
+ });
172
+
173
+ it("redirects to /login when user is not authenticated", async () => {
174
+ // Arrange
175
+ mockGetUser.mockResolvedValue({ data: { user: null } });
176
+ const { default: HomePage } = await import("../(protected)/page");
177
+
178
+ // Act + Assert
179
+ await expect(HomePage()).rejects.toThrow("NEXT_REDIRECT");
180
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
181
+ });
182
+
183
+ it("renders dashboard when user is authenticated", async () => {
184
+ // Arrange
185
+ mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
186
+ const { default: HomePage } = await import("../(protected)/page");
187
+
188
+ // Act
189
+ const result = await HomePage();
190
+
191
+ // Assert
192
+ expect(result).toBeTruthy();
193
+ });
194
+ });
195
+ `;
196
+ function buildSkipSet() {
197
+ return /* @__PURE__ */ new Set([
198
+ "src/features/todos",
199
+ "src/e2e/todos.spec.ts"
200
+ ]);
201
+ }
202
+ async function scaffold(projectName, skipInstall, skipMcp = false, template = "example") {
87
203
  const targetDir = join(process.cwd(), projectName);
88
204
  if (existsSync(targetDir)) {
89
205
  p3.cancel(`Directory "${projectName}" already exists`);
@@ -91,9 +207,19 @@ async function scaffold(projectName, skipInstall) {
91
207
  }
92
208
  const templateDir = getTemplateDir();
93
209
  const vars = { projectName };
210
+ const skip = template === "empty" ? buildSkipSet() : /* @__PURE__ */ new Set();
94
211
  const spinner2 = p3.spinner();
95
212
  spinner2.start("Copying template files");
96
- copyDir(templateDir, targetDir, vars);
213
+ copyDir(templateDir, targetDir, vars, skip, templateDir);
214
+ if (template === "empty") {
215
+ const pageDir = join(targetDir, "src/app/(protected)");
216
+ mkdirSync(pageDir, { recursive: true });
217
+ writeFileSync(join(pageDir, "page.tsx"), EMPTY_APP_PAGE);
218
+ writeFileSync(join(targetDir, "src/shared/db/schema.ts"), EMPTY_APP_SCHEMA);
219
+ const testDir = join(targetDir, "src/app/__tests__");
220
+ mkdirSync(testDir, { recursive: true });
221
+ writeFileSync(join(testDir, "protected-page.test.tsx"), EMPTY_APP_PAGE_TEST);
222
+ }
97
223
  spinner2.stop("Template files copied");
98
224
  const envExamplePath = join(targetDir, ".env.example");
99
225
  const envLocalPath = join(targetDir, ".env.local");
@@ -154,6 +280,34 @@ async function scaffold(projectName, skipInstall) {
154
280
  } catch {
155
281
  spinner2.stop("Husky setup failed \u2014 run manually: pnpm exec husky");
156
282
  }
283
+ if (!skipMcp) {
284
+ spinner2.start("Installing mcp-memory-service");
285
+ try {
286
+ const pipCmd = (() => {
287
+ try {
288
+ execSync("pip3 --version", { stdio: "pipe" });
289
+ return "pip3";
290
+ } catch {
291
+ try {
292
+ execSync("pip --version", { stdio: "pipe" });
293
+ return "pip";
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+ })();
299
+ if (pipCmd) {
300
+ execSync(`${pipCmd} install mcp-memory-service`, { cwd: targetDir, stdio: "pipe" });
301
+ spinner2.stop("mcp-memory-service installed");
302
+ } else {
303
+ spinner2.stop("pip not found \u2014 skipping mcp-memory-service");
304
+ p3.log.warn("Install Python and run: pip install mcp-memory-service");
305
+ }
306
+ } catch {
307
+ spinner2.stop("mcp-memory-service installation failed");
308
+ p3.log.warn("Install manually: pip install mcp-memory-service");
309
+ }
310
+ }
157
311
  success(pc2.bold(`\xA1Proyecto "${projectName}" creado!`));
158
312
  console.log(`
159
313
  ${pc2.dim("Siguientes pasos:")}`);
@@ -170,10 +324,10 @@ async function scaffold(projectName, skipInstall) {
170
324
 
171
325
  // src/index.ts
172
326
  var program = new Command();
173
- program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).action(async (projectName, opts) => {
327
+ program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).option("--skip-mcp", "Skip mcp-memory-service installation", false).action(async (projectName, opts) => {
174
328
  try {
175
329
  const options = await runCli(projectName, opts.skipInstall);
176
- await scaffold(options.projectName, options.skipInstall);
330
+ await scaffold(options.projectName, options.skipInstall, opts.skipMcp, options.template);
177
331
  outro2("Happy hacking! \u{1F680}");
178
332
  } catch (err) {
179
333
  p4.cancel(err instanceof Error ? err.message : "Something went wrong");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,10 @@ readonly: false
7
7
 
8
8
  # Backend Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to understand the existing schema, features, and proven Server Action patterns before writing new code. Use the canonical pattern references to copy the correct structure.
13
+
10
14
  ## Responsibilities
11
15
  - Build Next.js Server Actions, API routes, and middleware
12
16
  - Define Drizzle ORM schemas and manage migrations via CLI
@@ -23,6 +27,20 @@ readonly: false
23
27
  - Default to **Node.js runtime** for Server Actions and API routes
24
28
  - Use **Edge runtime** only when latency is critical and no Node.js APIs are needed
25
29
 
30
+ ## RLS Policy Requirement
31
+
32
+ RLS is not optional and is never a post-implementation step. When you create a new table:
33
+
34
+ 1. Enable RLS on the table in Supabase
35
+ 2. Write the RLS policies before the feature is considered done. Default template:
36
+ ```sql
37
+ -- Users can only access their own rows
38
+ CREATE POLICY "users_own_rows" ON <table>
39
+ FOR ALL USING (user_id = auth.uid());
40
+ ```
41
+ 3. Document the policy in the architecture snapshot under the table entry
42
+ 4. Never leave "add RLS later" as a TODO — a table without RLS is a security vulnerability
43
+
26
44
  ## Dev Server
27
45
  - Before running `next dev`, check `.next/dev/lock`. If it exists and the PID is alive, an instance is already running — use that server's URL instead of starting a new one.
28
46
  - Enable `logging.browserToTerminal` in `next.config.ts` for terminal-based debugging (recommended: `'error'` for production-like sessions, `true` during active development).
@@ -7,6 +7,10 @@ readonly: true
7
7
 
8
8
  # Business Intelligence Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to understand existing features before asking discovery questions. Use it to identify relationships with existing data and patterns.
13
+
10
14
  ## Responsibilities
11
15
  - Discover requirements through user questions before defining anything
12
16
  - Write functional issues describing user-visible behavior
@@ -20,10 +24,21 @@ readonly: true
20
24
 
21
25
  Never assume requirements. Always ask first.
22
26
 
27
+ Complete ALL of these before writing a spec:
28
+
23
29
  1. **Problem & audience** — "What problem does this solve? Who experiences it?"
24
30
  2. **User flows** — "Walk me through the happy path. What happens on error?"
25
31
  3. **Edge cases & constraints** — "What are the limits? What should NOT happen?"
26
- 4. **Confirm understanding** — Restate what you heard and ask for approval before writing anything
32
+ 4. **Field constraints** — "What are the length limits, allowed formats, required vs optional fields?"
33
+ 5. **Volume & scale** — "How many records are expected? Do you need search or pagination?"
34
+ 6. **File/upload specifics** — (if applicable) "What file types and size limits are allowed?"
35
+ 7. **Privacy & access** — "Who can see this data? Is it per-user or shared?"
36
+ 8. **Relationship to existing features** — (informed by the snapshot) "Does this link to existing data?"
37
+ 9. **Confirm understanding** — Restate what you heard and ask for approval before writing anything
38
+
39
+ **Minimum gate:** Cover at least items 1–3 + any that apply from 4–8 before writing.
40
+
41
+ If the user says "just do it" without answering, document all assumptions explicitly in an `## Assumptions` section at the top of the spec.
27
42
 
28
43
  Only after the user confirms your understanding should you produce a functional issue.
29
44
 
@@ -59,6 +59,9 @@ glab --version 2>/dev/null && echo "GitLab"
59
59
  - [ ] Semantic HTML
60
60
  - [ ] ARIA labels where needed
61
61
 
62
+ ### Styling
63
+ - [ ] No hardcoded Tailwind palette colors — only design tokens from `tailwind.css`
64
+
62
65
  ## Output Format
63
66
  ```
64
67
  ## Review: <branch/PR name>
@@ -7,6 +7,10 @@ readonly: false
7
7
 
8
8
  # Frontend Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to see which shadcn/ui components are already installed before running `shadcn add`. Use the canonical component test reference to follow the established testing pattern.
13
+
10
14
  ## Responsibilities
11
15
  - Build accessible React components with shadcn/ui and Tailwind v4
12
16
  - Enforce Server vs Client component boundaries
@@ -35,6 +39,19 @@ export function InteractiveComponent() {
35
39
  }
36
40
  ```
37
41
 
42
+ ## Common UI Patterns
43
+
44
+ Use these shadcn components for standard layouts — never roll custom alternatives:
45
+
46
+ | Pattern | shadcn Component | Notes |
47
+ |---------|-----------------|-------|
48
+ | Master-detail (table + side panel) | `ResizablePanelGroup` + `ResizablePanel` | Or CSS grid for fixed layouts |
49
+ | Dialog / modal form | `Dialog`, `DialogContent`, `DialogHeader` | Never use native `<dialog>` — breaks in jsdom |
50
+ | Data table | `Table`, `TableHeader`, `TableRow`, `TableCell` | Or `DataTable` pattern from shadcn docs |
51
+ | Empty state | Centered `<p>` in Spanish inside the table container | Always handle — never leave blank |
52
+ | Loading/pending | `Button` with `loading` prop, or `Spinner` | Use `useTransition` for action pending state |
53
+ | Form with validation | `Form` + `FormField` from shadcn + `zodResolver` | See `forms.mdc` |
54
+
38
55
  ## Toast Feedback
39
56
 
40
57
  All user-initiated actions that modify the database must show a toast (success or error) via `sonner`. Never let a mutation complete silently.
@@ -48,3 +65,4 @@ All user-initiated actions that modify the database must show a toast (success o
48
65
  - Write RTL tests for every component
49
66
  - Verify a11y before marking work done
50
67
  - No component file over 200 lines — split by responsibility
68
+ - Never use raw Tailwind palette colors (`red-500`, `gray-400`, etc.) — only design tokens from `@theme {}` in `tailwind.css`
@@ -9,6 +9,10 @@ readonly: false
9
9
 
10
10
  # Technical Lead Agent
11
11
 
12
+ ## First Step
13
+
14
+ Read `.cursor/memory/architecture-snapshot.md` to understand the current project state before planning. Use it to identify which patterns to reuse and what already exists.
15
+
12
16
  ## Responsibilities
13
17
  - Act as the default entry point for all requests
14
18
  - Classify requests and delegate to the appropriate specialist agent(s)
@@ -7,12 +7,30 @@ readonly: false
7
7
 
8
8
  # Test & QA Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to find the canonical test references. Import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock` — never recreate the mock chain from scratch.
13
+
10
14
  ## TDD Workflow
15
+
11
16
  1. **RED** — write failing test that describes the desired behavior
12
17
  2. **GREEN** — write minimum code to make test pass
13
18
  3. **REFACTOR** — clean up while keeping tests green
14
19
  4. **VERIFY** — run `pnpm test:coverage` and confirm 100%
15
20
 
21
+ ## Verification Gates (mandatory — show output, do not just claim)
22
+
23
+ Each phase requires pasting the actual terminal output before proceeding:
24
+
25
+ | Gate | Command | Expected output |
26
+ |------|---------|-----------------|
27
+ | After RED | `pnpm test:unit` | All new tests **FAIL** (red) |
28
+ | After GREEN | `pnpm test:unit` | All tests **PASS** (green) |
29
+ | After VERIFY | `pnpm test:coverage` | 100% across all metrics |
30
+
31
+ **Never say "tests pass" or "tests fail" without showing the actual output.**
32
+ **Do NOT write any implementation code until the RED gate output confirms failures.**
33
+
16
34
  ## Key Rules (auto-loaded by file context)
17
35
  - Testing standards, AAA pattern, coverage: `testing.mdc`
18
36
  - TDD iron rules: `coding-standards.mdc`
@@ -30,6 +48,24 @@ After every change:
30
48
  2. If below 100%, add missing tests
31
49
  3. Never mark work complete without full coverage
32
50
 
51
+ ## Supabase Mock Pattern
52
+
53
+ Always use the shared helpers — never inline mock chain construction:
54
+
55
+ ```typescript
56
+ import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
57
+
58
+ // Unauthenticated
59
+ const { mockClient } = makeSupabaseMock({ user: null });
60
+
61
+ // Authenticated + DB success
62
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
63
+ mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
64
+
65
+ // DB error
66
+ mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
67
+ ```
68
+
33
69
  ## Test Plan Compliance
34
70
 
35
71
  When the Technical Lead provides a test plan, follow it:
@@ -37,7 +73,7 @@ When the Technical Lead provides a test plan, follow it:
37
73
  - Flag missing edge cases to the TL **before** adding unplanned tests — do not expand scope unilaterally
38
74
  - If the plan is ambiguous or incomplete, ask for clarification rather than guessing
39
75
 
40
- For Server Action test templates, see `create-feature` skill references.
76
+ For the full Server Action test template, see `create-feature` skill references.
41
77
 
42
78
  ## Guardrails
43
79
  - Never write implementation without a failing test first
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "memory": {
4
+ "command": "python3",
5
+ "args": ["-m", "mcp_memory_service", "server"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,50 @@
1
+ # Architecture Snapshot
2
+
3
+ > Auto-updated after each feature. Agents MUST read this before exploring the codebase.
4
+
5
+ ## Installed shadcn/ui Components
6
+
7
+ button, card, input, label, spinner
8
+
9
+ _Add new components here after `shadcn add <component>`_
10
+
11
+ ## DB Schema
12
+
13
+ | Table | Columns |
14
+ |-------|---------|
15
+ | `profiles` | id (uuid PK), email (text unique), createdAt, updatedAt |
16
+ | `todos` | id (uuid PK), userId (uuid), title (text), completed (bool), createdAt |
17
+
18
+ _Add new tables here after `pnpm db:generate && pnpm db:migrate`_
19
+
20
+ ## Existing Features
21
+
22
+ | Feature | Path | Description |
23
+ |---------|------|-------------|
24
+ | auth | `src/features/auth/` | Login/logout, cookie-based sessions via Supabase |
25
+ | todos | `src/features/todos/` | CRUD todos with Supabase RLS |
26
+
27
+ _Add new features here after implementation_
28
+
29
+ ## Canonical Pattern References
30
+
31
+ Read these files to understand the project's proven patterns before writing new code:
32
+
33
+ | Pattern | File |
34
+ |---------|------|
35
+ | Server Action | `src/features/todos/actions/todos.action.ts` |
36
+ | Query (repository) | `src/features/todos/queries/todos.queries.ts` |
37
+ | Client component | `src/features/todos/components/todo-list.tsx` |
38
+ | Action test | `src/features/todos/__tests__/todos.action.test.ts` |
39
+ | Component test | `src/features/todos/__tests__/todo-list.test.tsx` |
40
+ | Supabase mock chain | `src/shared/test-utils/supabase-mock.ts` |
41
+
42
+ ## Key Rules (summary)
43
+
44
+ - Runtime queries: Supabase client only — Drizzle is schema/migrations only
45
+ - All mutations: Server Actions returning `ActionResult` from `@/shared/lib/action-result`
46
+ - Auth check first in every Server Action: `supabase.auth.getUser()`
47
+ - Toast feedback required for every mutation (success + error) via `sonner`
48
+ - RLS must be enabled on every table before a feature is considered complete
49
+ - UI text: Spanish | Code-level text: English (test descriptions, variable names)
50
+ - 100% test coverage — no exceptions
@@ -14,18 +14,9 @@ globs: ["src/**/*.ts", "src/**/*.tsx"]
14
14
  - **No `!` non-null assertions** — use proper null checks
15
15
  - **No `eslint-disable` inline comments** — fix the code instead. If a rule conflict is truly unavoidable (e.g. third-party API with no non-deprecated alternative), add a file-level override in `eslint.config.ts` with a `// TODO` comment explaining why.
16
16
 
17
- ## TDD (Test-Driven Development)
18
- 1. **RED**: Write a failing test first
19
- 2. **GREEN**: Write minimum code to pass the test
20
- 3. **REFACTOR**: Improve code while tests stay green
21
-
22
- Never write implementation before tests exist.
23
-
24
- ## TDD — Iron Rule
25
- - Tests define the contract. **NEVER modify a test to make it pass.**
26
- - If a test fails, the implementation is wrong — fix the code, not the test.
27
- - Only modify tests to: add new tests (RED), restructure without weakening (REFACTOR).
28
- - Weakening a test (removing assertions, loosening matchers, adding `.skip`) is a rejection-worthy offense.
17
+ ## TDD
18
+
19
+ Follow the RED → GREEN REFACTOR cycle. See `testing.mdc` for the full rules and iron rules. Summary: never write implementation without a prior failing test.
29
20
 
30
21
  ## SOLID Principles
31
22
  - **S**ingle Responsibility: one reason to change per module
@@ -24,6 +24,13 @@ globs: ["src/**/components/**"]
24
24
  - No inline styles, no CSS modules
25
25
  - Use Tailwind utility classes only
26
26
 
27
+ ## Design Tokens (Required)
28
+ - Always use semantic token classes (`bg-primary`, `text-destructive`, `text-muted-foreground`, `border-border`, etc.)
29
+ - Never use raw Tailwind palette colors (`bg-blue-500`, `text-gray-700`, `text-red-500`, etc.)
30
+ - All colors must reference CSS variables defined in `@theme {}` in `tailwind.css`
31
+ - Available semantic colors: primary, secondary, accent, destructive, success, warning, info, muted, background, foreground, border, input, ring
32
+ - If a needed color does not exist as a token, add it to `@theme {}` in `tailwind.css` first
33
+
27
34
  ## Accessibility (Required)
28
35
  - Semantic HTML elements (`<button>`, `<nav>`, `<main>`, etc.)
29
36
  - ARIA labels for interactive elements without visible text
@@ -5,23 +5,6 @@ globs: ["src/shared/db/migrations/**"]
5
5
 
6
6
  # Migration Files — Generated Artifacts
7
7
 
8
- Files in `src/shared/db/migrations/` are **auto-generated** by the Drizzle CLI. Do not create, edit, or delete them by hand.
8
+ Files in `src/shared/db/migrations/` are **auto-generated**. See `supabase.mdc` for the full migration workflow and CLI commands.
9
9
 
10
- ## Correct Workflow
11
-
12
- 1. **Edit the schema**: `src/shared/db/schema.ts`
13
- 2. **Generate the migration**: `pnpm db:generate`
14
- 3. **Review** the generated SQL file in `src/shared/db/migrations/`
15
- 4. **Apply the migration**: `pnpm db:migrate`
16
-
17
- ## Available CLI Commands
18
-
19
- | Command | Purpose |
20
- |---|---|
21
- | `pnpm db:generate` | Generate a new migration SQL file from schema changes |
22
- | `pnpm db:migrate` | Apply pending migrations to the database |
23
- | `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
24
-
25
- ## Why
26
-
27
- Hand-editing migration files causes schema drift and breaks the migration history. The CLI is the only safe source of truth for migration SQL.
10
+ **Never create, edit, or delete migration files by hand.**
@@ -48,8 +48,20 @@ const users = await db.select().from(usersTable); // WRONG — bypasses RLS
48
48
 
49
49
  ## RLS Mandate
50
50
  - Enable RLS on ALL tables in Supabase dashboard
51
- - Every table must have explicit policies
52
- - Test RLS policies in migrations
51
+ - Every table must have explicit policies — RLS is not a post-step
52
+ - Default policy template: `CREATE POLICY "users_own_rows" ON <table> FOR ALL USING (user_id = auth.uid());`
53
+
54
+ ## Migrations
55
+
56
+ Files in `src/shared/db/migrations/` are **auto-generated** by Drizzle CLI — never create, edit, or delete them directly.
57
+
58
+ | Command | Purpose |
59
+ |---|---|
60
+ | `pnpm db:generate` | Generate migration SQL from schema changes |
61
+ | `pnpm db:migrate` | Apply pending migrations to the database |
62
+ | `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
63
+
64
+ Workflow: edit `src/shared/db/schema.ts` → `pnpm db:generate` → review SQL → `pnpm db:migrate`
53
65
 
54
66
  ## Environment Variables
55
67
  - `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY` — client-safe, used in browser + server
@@ -5,6 +5,17 @@ description: Scaffold a new feature following TDD and project conventions. Use w
5
5
 
6
6
  # Create Feature Skill
7
7
 
8
+ > **Invoke as:** `/create-feature @.requirements/<feature-name>.md`
9
+ > This skill is orchestrated by the `@technical-lead` agent — do NOT add `use @technical-lead` to the invocation, it is redundant.
10
+
11
+ ## IMPORTANT: Run this in a FRESH conversation
12
+
13
+ If this conversation already contains requirements gathering (BI agent output), **start a new conversation** before running `/create-feature`. Reusing the same context risks hitting the token limit mid-implementation.
14
+
15
+ If context usage exceeds 60% at any point during this skill, stop and tell the user to continue in a new conversation, referencing this plan file.
16
+
17
+ ---
18
+
8
19
  ## Process
9
20
 
10
21
  ### 1. Requirements First
@@ -13,7 +24,15 @@ description: Scaffold a new feature following TDD and project conventions. Use w
13
24
  - **If no spec exists, call @business-intelligence** to run the discovery process and produce a functional issue in `.requirements/<feature-name>.md`
14
25
  - Wait for the functional issue to be written and confirmed by the user before proceeding to Step 2
15
26
 
16
- ### 2. Create Feature Structure
27
+ ### 2. Read the Architecture Snapshot
28
+
29
+ Read `.cursor/memory/architecture-snapshot.md`. This tells you:
30
+ - Which shadcn/ui components are already installed (no need to `shadcn add` duplicates)
31
+ - The current DB schema (what tables exist, what to extend)
32
+ - Existing features and their paths (avoid recreating patterns)
33
+ - Canonical pattern references (copy from these, do not reinvent)
34
+
35
+ ### 3. Create Feature Structure
17
36
  ```
18
37
  src/features/<feature-name>/
19
38
  ├── components/
@@ -25,7 +44,7 @@ src/features/<feature-name>/
25
44
  └── __tests__/
26
45
  ```
27
46
 
28
- ### 3. Pre-Test Setup (do this before writing any tests)
47
+ ### 4. Pre-Test Setup (do this before writing any tests)
29
48
 
30
49
  Update `vitest.config.ts` to exclude files that cannot be meaningfully tested in jsdom:
31
50
  - Drizzle schema file(s) (`src/shared/db/schema.ts`)
@@ -42,32 +61,76 @@ exclude: [
42
61
 
43
62
  Configuring exclusions upfront prevents reactive coverage fixes mid-implementation.
44
63
 
45
- ### 4. TDD: RED Phase
46
- Write ALL test files first:
64
+ ### 5. TDD: RED Phase
65
+
66
+ Write ALL test files first — zero implementation code at this stage:
47
67
  - `__tests__/<component>.test.tsx` — component tests
48
- - `__tests__/use-<feature>.test.ts` — hook tests
49
- - `__tests__/<action>.test.ts` — action tests (see `references/server-action-test-template.md`)
68
+ - `__tests__/use-<feature>.test.ts` — hook tests (if applicable)
69
+ - `__tests__/<action>.action.test.ts` — action tests (see `references/server-action-test-template.md`)
50
70
  - `__tests__/<feature>.queries.test.ts` — query tests
51
71
 
52
- Run `pnpm test:unit` — all tests must FAIL (RED).
72
+ Import mock helpers from `@/shared/test-utils/supabase-mock` — do not inline mock chains.
73
+
74
+ **BLOCKING GATE — do not proceed until this passes:**
75
+ Run `pnpm test:unit` and paste the output here. All new tests must **FAIL** (red). If any new test passes without implementation, the test is wrong — fix it before continuing.
76
+
77
+ ### 6. TDD: GREEN Phase
53
78
 
54
- ### 5. TDD: GREEN Phase
55
- Implement minimum code to pass each test:
56
- - Components → actions → hooks → API routes
79
+ Launch **@backend and @frontend in parallel** — they work on independent files:
57
80
 
58
- Run `pnpm test:unit` all tests must PASS (GREEN).
81
+ | @backend handles | @frontend handles |
82
+ |-----------------|-------------------|
83
+ | `actions/` | `components/` |
84
+ | `queries/` | `page.tsx` |
85
+ | `schema.ts` changes | shadcn component installs |
86
+ | RLS policies | Loading/empty states |
59
87
 
60
- ### 6. Refactor
88
+ Do NOT serialize these — they touch different files.
89
+
90
+ **BLOCKING GATE — do not proceed until this passes:**
91
+ Run `pnpm test:unit` and paste the output. All tests must **PASS** (green).
92
+
93
+ ### 7. Refactor
61
94
  Clean up while keeping tests green.
62
95
 
63
- ### 7. Verify
96
+ ### 8. Verify & Self-Check
97
+
98
+ Run all three and paste output:
64
99
  ```bash
65
- pnpm test:coverage # Must show 100%
100
+ pnpm test:coverage # Must show 100% across all metrics
66
101
  pnpm lint # Must pass with 0 warnings
67
102
  pnpm typecheck # Must pass with 0 errors
68
103
  ```
69
104
 
105
+ Before moving to the review gate, verify manually:
106
+ - [ ] No `any` types: `grep -r ": any" src/features/<feature>/`
107
+ - [ ] No `eslint-disable` comments
108
+ - [ ] All UI text in Spanish, all `it()`/`describe()` text in English
109
+ - [ ] No file over 200 lines
110
+ - [ ] No function over 20 lines
111
+ - [ ] AAA pattern (`// Arrange`, `// Act`, `// Assert`) in every test
112
+ - [ ] `ActionResult` returned from every Server Action mutation
113
+ - [ ] Toast feedback (`toast.success` / `toast.error`) for every mutation
114
+ - [ ] RLS policies written for every new table
115
+
116
+ ### 9. Review Gate
117
+
118
+ Run @code-reviewer and @security-researcher **in parallel** on the changed files. Fix any findings before marking the feature complete.
119
+
120
+ This step is not optional. The feature is not done until both reviewers pass.
121
+
122
+ ### 10. Update Architecture Snapshot
123
+
124
+ Update `.cursor/memory/architecture-snapshot.md`:
125
+ - Add new DB tables to the schema table
126
+ - Add new shadcn components to the installed list
127
+ - Add the new feature to the Existing Features table
128
+ - Add any new canonical pattern references that differ from existing ones
129
+
130
+ ---
131
+
70
132
  ## Guardrails
71
- - NEVER start implementation before tests exist
133
+ - NEVER start implementation before the RED gate is confirmed with actual test output
72
134
  - 100% coverage before marking done
73
135
  - Follow `features/* → shared/*` dependency direction
136
+ - RLS is mandatory for every new table — not a post-step
@@ -2,50 +2,102 @@
2
2
 
3
3
  Test Server Actions by mocking `createClient`, asserting on the returned `ActionResult`, and verifying side effects.
4
4
 
5
+ ## Setup
6
+
5
7
  ```typescript
6
- import { vi, it, expect, describe } from "vitest";
8
+ import { vi, it, expect, describe, beforeEach } from "vitest";
9
+ import { createClient } from "@/shared/lib/supabase/server";
10
+ import { revalidatePath } from "next/cache";
11
+ import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
7
12
  import { myAction } from "@/features/example/actions/my.action";
8
13
 
9
- vi.mock("@/shared/lib/supabase/server", () => ({
10
- createClient: vi.fn(),
11
- }));
14
+ vi.mock("@/shared/lib/supabase/server", () => ({ createClient: vi.fn() }));
12
15
  vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
13
16
 
14
- describe("myAction", () => {
15
- it("returns error when user is not authenticated", async () => {
16
- // Arrange
17
- const mockSupabase = {
18
- auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
19
- };
20
- vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
21
- const formData = new FormData();
22
-
23
- // Act
24
- const result = await myAction(formData);
25
-
26
- // Assert
27
- expect(result).toEqual({ status: "error", message: "No autenticado" });
28
- expect(revalidatePath).not.toHaveBeenCalled();
29
- });
30
-
31
- it("returns success and revalidates on valid input", async () => {
32
- // Arrange
33
- const mockUser = { id: "user-1" };
34
- const mockSupabase = {
35
- auth: { getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }) },
36
- from: vi.fn().mockReturnThis(),
37
- insert: vi.fn().mockResolvedValue({ error: null }),
38
- };
39
- vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
40
- const formData = new FormData();
41
- formData.set("name", "Test Item");
42
-
43
- // Act
44
- const result = await myAction(formData);
45
-
46
- // Assert
47
- expect(result).toEqual({ status: "success", message: expect.any(String) });
48
- expect(revalidatePath).toHaveBeenCalledWith("/");
49
- });
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+ ```
21
+
22
+ ## Unauthenticated case (required in every action test)
23
+
24
+ ```typescript
25
+ it("returns error when user is not authenticated", async () => {
26
+ // Arrange
27
+ const { mockClient } = makeSupabaseMock({ user: null });
28
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
29
+ const formData = new FormData();
30
+
31
+ // Act
32
+ const result = await myAction(formData);
33
+
34
+ // Assert
35
+ expect(result).toEqual({ status: "error", message: "No autenticado" });
36
+ expect(revalidatePath).not.toHaveBeenCalled();
50
37
  });
51
38
  ```
39
+
40
+ ## Happy path (insert example)
41
+
42
+ ```typescript
43
+ it("returns success and revalidates on valid input", async () => {
44
+ // Arrange
45
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
46
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
47
+ mockFrom.mockReturnValue(makeChain({ error: null }));
48
+ const formData = new FormData();
49
+ formData.set("title", "My item");
50
+
51
+ // Act
52
+ const result = await myAction(formData);
53
+
54
+ // Assert
55
+ expect(result).toEqual({ status: "success", message: expect.any(String) });
56
+ expect(revalidatePath).toHaveBeenCalledWith("/");
57
+ });
58
+ ```
59
+
60
+ ## DB error case
61
+
62
+ ```typescript
63
+ it("returns error when db insert fails", async () => {
64
+ // Arrange
65
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
66
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
67
+ mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
68
+ const formData = new FormData();
69
+ formData.set("title", "My item");
70
+
71
+ // Act
72
+ const result = await myAction(formData);
73
+
74
+ // Assert
75
+ expect(result).toEqual({ status: "error", message: expect.any(String) });
76
+ expect(revalidatePath).not.toHaveBeenCalled();
77
+ });
78
+ ```
79
+
80
+ ## Validation error case
81
+
82
+ ```typescript
83
+ it("returns error when required field is missing", async () => {
84
+ // Arrange
85
+ const { mockClient } = makeSupabaseMock({ user: { id: "user-1" } });
86
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
87
+ const formData = new FormData(); // no fields set
88
+
89
+ // Act
90
+ const result = await myAction(formData);
91
+
92
+ // Assert
93
+ expect(result).toEqual({ status: "error", message: expect.any(String) });
94
+ });
95
+ ```
96
+
97
+ ## Key Rules
98
+
99
+ - Always import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock`
100
+ - Every action test must cover: unauthenticated, validation error, DB error, happy path
101
+ - `mockFrom` returns a chain — each chained method (`.select()`, `.insert()`, etc.) returns the same chain object
102
+ - `then` on the chain resolves with the value you pass to `makeChain()`
103
+ - Never use `mockReturnThis()` — it does not match Supabase's actual chaining API
@@ -46,4 +46,14 @@ The `/create-feature` skill reads the requirements file directly, so the BI conv
46
46
  - Components: PascalCase `.tsx`
47
47
  - Tests: colocated in `__tests__/`, named `name.test.ts`
48
48
 
49
+ # MCP Memory Service
50
+
51
+ This project uses [mcp-memory-service](https://github.com/doobidoo/mcp-memory-service) for persistent semantic memory across sessions.
52
+ Config is shared via `.cursor/mcp.json` (symlinked to `.mcp.json`).
53
+
54
+ If not installed automatically during scaffolding:
55
+ ```bash
56
+ pip install mcp-memory-service
57
+ ```
58
+
49
59
  # Maintenance Notes
@@ -21,7 +21,7 @@ export default async function HomePage() {
21
21
  <form action={logoutAction}>
22
22
  <button
23
23
  type="submit"
24
- className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
24
+ className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10"
25
25
  >
26
26
  Cerrar sesión
27
27
  </button>
@@ -34,7 +34,7 @@ export function AddTodoForm() {
34
34
  placeholder="Nueva tarea…"
35
35
  required
36
36
  disabled={isPending}
37
- className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400"
37
+ className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
38
38
  aria-label="Nueva tarea"
39
39
  />
40
40
  <Button type="submit" loading={isPending}>
@@ -17,7 +17,7 @@ export function TodoList({ items }: TodoListProps) {
17
17
 
18
18
  if (items.length === 0) {
19
19
  return (
20
- <p className="mt-4 text-center text-sm text-gray-500">
20
+ <p className="mt-4 text-center text-sm text-muted-foreground">
21
21
  No tienes tareas. ¡Agrega una!
22
22
  </p>
23
23
  );
@@ -44,10 +44,10 @@ export function TodoList({ items }: TodoListProps) {
44
44
  }}
45
45
  aria-label={`Marcar "${todo.title}" como completada`}
46
46
  />
47
- <span className={todo.completed ? "line-through text-gray-400" : ""}>
47
+ <span className={todo.completed ? "line-through text-muted-foreground" : ""}>
48
48
  {todo.title}
49
49
  </span>
50
- {isPending && <Spinner className="size-4 text-gray-400" />}
50
+ {isPending && <Spinner className="size-4 text-muted-foreground" />}
51
51
  </label>
52
52
  <button
53
53
  type="button"
@@ -59,7 +59,7 @@ export function TodoList({ items }: TodoListProps) {
59
59
  else toast.success(result.message);
60
60
  });
61
61
  }}
62
- className="rounded px-2 py-1 text-sm text-red-500 hover:bg-red-50"
62
+ className="rounded px-2 py-1 text-sm text-destructive hover:bg-destructive/10"
63
63
  aria-label={`Eliminar "${todo.title}"`}
64
64
  >
65
65
  Eliminar
@@ -19,7 +19,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
19
19
  return (
20
20
  <QueryClientProvider client={queryClient}>
21
21
  {children}
22
- <Toaster />
22
+ <Toaster
23
+ toastOptions={{
24
+ classNames: {
25
+ success: "!bg-success !text-success-foreground !border-success",
26
+ error: "!bg-destructive !text-destructive-foreground !border-destructive",
27
+ warning: "!bg-warning !text-warning-foreground !border-warning",
28
+ info: "!bg-info !text-info-foreground !border-info",
29
+ },
30
+ }}
31
+ />
23
32
  </QueryClientProvider>
24
33
  );
25
34
  }
@@ -0,0 +1,59 @@
1
+ import { vi } from "vitest";
2
+
3
+ type Chain = Record<string, ReturnType<typeof vi.fn>>;
4
+
5
+ /**
6
+ * Creates a chainable Supabase query mock.
7
+ *
8
+ * Usage:
9
+ * const chain = makeChain({ data: [...], error: null });
10
+ * mockSupabase.from.mockReturnValue(chain);
11
+ *
12
+ * Supports: .select() .insert() .update() .delete() .eq() .single() .order()
13
+ * Each method returns the same chain, so calls can be chained arbitrarily.
14
+ * The chain resolves with `resolvedValue` when awaited.
15
+ */
16
+ export function makeChain(resolvedValue: unknown): Chain {
17
+ const chain: Chain = {};
18
+ const methods = [
19
+ "select",
20
+ "insert",
21
+ "update",
22
+ "delete",
23
+ "eq",
24
+ "single",
25
+ "order",
26
+ "limit",
27
+ "match",
28
+ ];
29
+ for (const method of methods) {
30
+ chain[method] = vi.fn().mockReturnValue(chain);
31
+ }
32
+ chain["then"] = vi.fn((cb: (v: unknown) => unknown) => cb(resolvedValue));
33
+ return chain;
34
+ }
35
+
36
+ /**
37
+ * Creates a minimal Supabase client mock with auth + from.
38
+ *
39
+ * Usage:
40
+ * const { mockClient, mockFrom } = makeSupabaseMock({ user: mockUser });
41
+ * vi.mocked(createClient).mockResolvedValue(mockClient as never);
42
+ * mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
43
+ */
44
+ export function makeSupabaseMock(authResult: {
45
+ user: { id: string } | null;
46
+ error?: unknown;
47
+ }) {
48
+ const mockFrom = vi.fn();
49
+ const mockClient = {
50
+ auth: {
51
+ getUser: vi.fn().mockResolvedValue({
52
+ data: { user: authResult.user },
53
+ error: authResult.error ?? null,
54
+ }),
55
+ },
56
+ from: mockFrom,
57
+ };
58
+ return { mockClient, mockFrom };
59
+ }
@@ -13,6 +13,12 @@
13
13
  --color-accent-foreground: #ffffff;
14
14
  --color-destructive: hsl(0 84.2% 60.2%);
15
15
  --color-destructive-foreground: #ffffff;
16
+ --color-success: hsl(142 71% 45%);
17
+ --color-success-foreground: #ffffff;
18
+ --color-warning: hsl(38 92% 50%);
19
+ --color-warning-foreground: #ffffff;
20
+ --color-info: hsl(199 89% 48%);
21
+ --color-info-foreground: #ffffff;
16
22
  --color-border: #e5e5e5;
17
23
  --color-input: #e5e5e5;
18
24
  --color-ring: #022633;