@cogito.ai/cli 0.3.1 → 0.3.2

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.
Files changed (41) hide show
  1. package/README.md +181 -0
  2. package/dist/templates/web-nextjs/.env.example +4 -0
  3. package/dist/templates/web-nextjs/.vscode/settings.json +3 -0
  4. package/dist/templates/web-nextjs/README.md +25 -1
  5. package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
  6. package/dist/templates/web-nextjs/apps/docs/.source/server.ts +4 -3
  7. package/dist/templates/web-nextjs/apps/docs/content/docs/features/auth.mdx +139 -0
  8. package/dist/templates/web-nextjs/apps/web/components.json +25 -0
  9. package/dist/templates/web-nextjs/apps/web/messages/en.json +28 -0
  10. package/dist/templates/web-nextjs/apps/web/messages/zh.json +28 -0
  11. package/dist/templates/web-nextjs/apps/web/middleware.ts +53 -9
  12. package/dist/templates/web-nextjs/apps/web/package.json +13 -1
  13. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +142 -0
  14. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +151 -0
  15. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +42 -0
  16. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/layout.tsx +22 -0
  17. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +129 -3
  18. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +2 -4
  19. package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +21 -0
  20. package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +76 -0
  21. package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +58 -0
  22. package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +154 -0
  23. package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +20 -0
  24. package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +20 -0
  25. package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +50 -0
  26. package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +9 -0
  27. package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +14 -0
  28. package/dist/templates/web-nextjs/apps/web/src/features/auth/__contract__.ts +14 -0
  29. package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +86 -0
  30. package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +1 -0
  31. package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +12 -0
  32. package/dist/templates/web-nextjs/apps/web/src/i18n/request.ts +3 -1
  33. package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +63 -0
  34. package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +38 -0
  35. package/dist/templates/web-nextjs/apps/web/src/infra/providers.ts +6 -0
  36. package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +6 -0
  37. package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +20 -0
  38. package/dist/templates/web-nextjs/apps/web/src/styles/shadcn-tailwind.css +95 -0
  39. package/dist/templates/web-nextjs/apps/web/src/styles/tw-animate.css +1 -0
  40. package/dist/templates/web-nextjs/pnpm-lock.yaml +2327 -17
  41. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # AgentDock CLI
2
+
3
+ **`@cogito.ai/cli`** — Scaffold production-ready AI coding agent projects for humans, CI pipelines, and AI agents.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@cogito.ai/cli)](https://www.npmjs.com/package/@cogito.ai/cli)
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Interactive mode (for humans)
13
+ npx @cogito.ai/cli init
14
+
15
+ # Headless mode (for CI / AI agents)
16
+ npx @cogito.ai/cli init --name my-app --template web-nextjs --pm pnpm --json
17
+
18
+ # Start MCP Stdio server (for AI agents via Model Context Protocol)
19
+ npx @cogito.ai/cli mcp
20
+ ```
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Run without installing (recommended)
26
+ npx @cogito.ai/cli <command>
27
+
28
+ # Global install
29
+ npm install -g @cogito.ai/cli
30
+ pnpm add -g @cogito.ai/cli
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Commands
36
+
37
+ ### `agentdock init`
38
+
39
+ Scaffold a new project from a template. Auto-detects the environment:
40
+
41
+ - **TTY** → interactive prompts (human mode)
42
+ - **Non-TTY / `--silent` / `--json`** → headless execution (agent/CI mode)
43
+
44
+ | Flag | Type | Default | Description |
45
+ | ---- | ---- | ------- | ----------- |
46
+ | `--name` | string | required | Project name and target directory name |
47
+ | `--template` | string | required | Template ID (e.g. `web-nextjs`) |
48
+ | `--pm` | string | `pnpm` | Package manager: `pnpm` / `npm` / `yarn` / `bun` |
49
+ | `--dir` | string | `./<name>` | Target directory (absolute or relative to cwd) |
50
+ | `--json` | boolean | `false` | Output NDJSON result to stdout |
51
+ | `--silent` | boolean | `false` | Suppress all output |
52
+
53
+ **JSON output (success):**
54
+ ```json
55
+ {"ok":true,"targetDir":"/path/to/my-app","name":"my-app","template":"web-nextjs"}
56
+ ```
57
+
58
+ **JSON output (failure):**
59
+ ```json
60
+ {"ok":false,"error":"TARGET_DIR_EXISTS","message":"Directory already exists: /path/to/my-app"}
61
+ ```
62
+
63
+ **Error codes:** `MISSING_ARG` · `TEMPLATE_NOT_FOUND` · `TARGET_DIR_EXISTS` · `CLI_VERSION_OUTDATED` · `SCAFFOLD_FAILED`
64
+
65
+ ---
66
+
67
+ ### `agentdock mcp`
68
+
69
+ Start an MCP (Model Context Protocol) Stdio server. Exposes CLI capabilities as tools directly callable by AI agents.
70
+
71
+ ```bash
72
+ agentdock mcp
73
+ ```
74
+
75
+ **Available tools:**
76
+
77
+ | Tool | Description |
78
+ | ---- | ----------- |
79
+ | `list_templates` | List all available project templates |
80
+ | `scaffold_project` | Scaffold a project into a target directory |
81
+
82
+ **VS Code Copilot MCP config (`.vscode/mcp.json`):**
83
+ ```json
84
+ {
85
+ "servers": {
86
+ "agentdock": {
87
+ "type": "stdio",
88
+ "command": "npx",
89
+ "args": ["@cogito.ai/cli", "mcp"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Available Templates
98
+
99
+ | Template ID | Description |
100
+ | ----------- | ----------- |
101
+ | `web-nextjs` | Next.js 16 + Supabase + next-intl + Tailwind CSS v4 + Vitest + Fumadocs — full-stack monorepo starter |
102
+
103
+ ---
104
+
105
+ ## Architecture
106
+
107
+ The CLI uses an **Adapter pattern** — a single executor core serves three consumer surfaces:
108
+
109
+ ```
110
+ agentdock init
111
+
112
+ ├── TTY detected ──→ Human Adapter (Clack interactive UI)
113
+ └── Non-TTY / flags ──→ Agent Adapter (structured JSON output)
114
+
115
+ agentdock mcp ──→ MCP Adapter (MCP Stdio Server)
116
+
117
+ All adapters share the same Core Executor (scaffold + registry)
118
+ ```
119
+
120
+ **Design principles:**
121
+
122
+ 1. **Headless-first** — the executor core has no TTY dependency; the human UI is a thin shell on top.
123
+ 2. **Structured output as protocol** — `--json` mode produces stable machine-parseable NDJSON, not natural language.
124
+ 3. **One capability, three surfaces** — `scaffold_project` is implemented once and projected to CLI flags, JSON mode, and MCP tool automatically.
125
+
126
+ **Tech stack:**
127
+
128
+ | Component | Role |
129
+ | --------- | ---- |
130
+ | [Bun](https://github.com/oven-sh/bun) | Build: single-file Node.js executable |
131
+ | [Citty](https://github.com/unjs/citty) | Command modeling: sub-commands, flag schema, lazy loading |
132
+ | [Clack](https://github.com/bombshell-dev/clack) | Human interaction: TTY prompts |
133
+ | [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) | Agent protocol: MCP Stdio server |
134
+ | [Changesets](https://github.com/changesets/changesets) | Release governance: semantic versioning + changelog |
135
+
136
+ ---
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ # Install dependencies
142
+ pnpm install
143
+
144
+ # Regenerate template registry
145
+ pnpm --filter @cogito.ai/cli generate-registry
146
+
147
+ # Run from source (no build needed)
148
+ npx tsx packages/cli/bin/agentdock.ts init
149
+
150
+ # Run tests
151
+ pnpm --filter @cogito.ai/cli test
152
+
153
+ # Build
154
+ pnpm --filter @cogito.ai/cli build
155
+ ```
156
+
157
+ ### Adding a template
158
+
159
+ 1. Create `templates/<id>/` with a complete project structure.
160
+ 2. Add `package.json` with `"agentdock": { "minCliVersion": "x.y.z" }`.
161
+ 3. Run `pnpm --filter @cogito.ai/cli generate-registry`.
162
+ 4. Publish a new CLI version — templates are bundled inside the npm package.
163
+
164
+ ### Publishing a new version
165
+
166
+ ```bash
167
+ pnpm changeset # describe the change
168
+ pnpm changeset version # bump version + update CHANGELOG
169
+ pnpm --filter @cogito.ai/cli build
170
+ pnpm --filter @cogito.ai/cli publish
171
+ ```
172
+
173
+ ---
174
+
175
+ ## License
176
+
177
+ MIT
178
+
179
+ ---
180
+
181
+ [中文文档 →](./README-zh.md)
@@ -20,3 +20,7 @@ SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
20
20
 
21
21
  # Public URL of this app (used for OG image URLs, absolute redirects, etc.)
22
22
  NEXT_PUBLIC_APP_URL=http://localhost:3000
23
+
24
+ # Default locale used for root redirects and i18n fallback
25
+ # Allowed values: en, zh
26
+ APP_DEFAULT_LOCALE=zh
@@ -0,0 +1,3 @@
1
+ {
2
+ "css.lint.unknownAtRules": "ignore"
3
+ }
@@ -71,7 +71,31 @@ Open `.env.local` and fill in your values:
71
71
  | `SUPABASE_SERVICE_ROLE_KEY` | Supabase Dashboard → Project Settings → API → service_role (server only) |
72
72
  | `NEXT_PUBLIC_APP_URL` | `http://localhost:3000` for local dev |
73
73
 
74
- ### 4. Start development server
74
+ ### 4. Set up Supabase Auth (required for login / signup)
75
+
76
+ **4a. Enable GitHub OAuth provider**
77
+
78
+ 1. Go to **Supabase Dashboard → Authentication → Providers → GitHub**.
79
+ 2. Toggle **Enable Sign in with GitHub** on.
80
+ 3. Create a GitHub OAuth App at [github.com/settings/developers](https://github.com/settings/developers):
81
+ - **Homepage URL**: `http://localhost:3000`
82
+ - **Authorization callback URL**: copy the **Callback URL (for OAuth)** shown in the Supabase GitHub provider dialog (e.g. `https://<project-ref>.supabase.co/auth/v1/callback`)
83
+ 4. Paste the GitHub **Client ID** and **Client Secret** back into the Supabase provider settings and click **Save**.
84
+
85
+ **4b. Add redirect URL**
86
+
87
+ 1. Go to **Supabase Dashboard → Authentication → URL Configuration**.
88
+ 2. Under **Redirect URLs**, click **Add URL** and add:
89
+ - `http://localhost:3000/auth/callback` ← for local development
90
+ - `https://<your-production-domain>/auth/callback` ← for production
91
+ 3. Under **Site URL** set `http://localhost:3000` (or your production URL).
92
+
93
+ **4c. (Optional) Email confirmation**
94
+
95
+ By default Supabase requires email confirmation before a user can sign in.
96
+ To disable during development: **Authentication → Email → Enable email confirmations** → toggle off.
97
+
98
+ ### 5. Start development server
75
99
 
76
100
  ```bash
77
101
  pnpm dev
@@ -7,6 +7,6 @@ const create = browser<typeof Config, import("fumadocs-mdx/runtime/types").Inter
7
7
  }
8
8
  }>();
9
9
  const browserCollections = {
10
- docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "changelog/index.mdx": () => import("../content/docs/changelog/index.mdx?collection=docs"), "features/hello.mdx": () => import("../content/docs/features/hello.mdx?collection=docs"), "roadmap/index.mdx": () => import("../content/docs/roadmap/index.mdx?collection=docs"), }),
10
+ docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "changelog/index.mdx": () => import("../content/docs/changelog/index.mdx?collection=docs"), "roadmap/index.mdx": () => import("../content/docs/roadmap/index.mdx?collection=docs"), "features/auth.mdx": () => import("../content/docs/features/auth.mdx?collection=docs"), "features/hello.mdx": () => import("../content/docs/features/hello.mdx?collection=docs"), }),
11
11
  };
12
12
  export default browserCollections;
@@ -1,6 +1,7 @@
1
1
  // @ts-nocheck
2
- import * as __fd_glob_8 from "../content/docs/roadmap/index.mdx?collection=docs"
3
- import * as __fd_glob_7 from "../content/docs/features/hello.mdx?collection=docs"
2
+ import * as __fd_glob_9 from "../content/docs/features/hello.mdx?collection=docs"
3
+ import * as __fd_glob_8 from "../content/docs/features/auth.mdx?collection=docs"
4
+ import * as __fd_glob_7 from "../content/docs/roadmap/index.mdx?collection=docs"
4
5
  import * as __fd_glob_6 from "../content/docs/changelog/index.mdx?collection=docs"
5
6
  import * as __fd_glob_5 from "../content/docs/index.mdx?collection=docs"
6
7
  import { default as __fd_glob_4 } from "../content/docs/roadmap/meta.json?collection=docs"
@@ -16,4 +17,4 @@ const create = server<typeof Config, import("fumadocs-mdx/runtime/types").Intern
16
17
  }
17
18
  }>({"doc":{"passthroughs":["extractedReferences"]}});
18
19
 
19
- export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "changelog/meta.json": __fd_glob_1, "decisions/meta.json": __fd_glob_2, "features/meta.json": __fd_glob_3, "roadmap/meta.json": __fd_glob_4, }, {"index.mdx": __fd_glob_5, "changelog/index.mdx": __fd_glob_6, "features/hello.mdx": __fd_glob_7, "roadmap/index.mdx": __fd_glob_8, });
20
+ export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "changelog/meta.json": __fd_glob_1, "decisions/meta.json": __fd_glob_2, "features/meta.json": __fd_glob_3, "roadmap/meta.json": __fd_glob_4, }, {"index.mdx": __fd_glob_5, "changelog/index.mdx": __fd_glob_6, "roadmap/index.mdx": __fd_glob_7, "features/auth.mdx": __fd_glob_8, "features/hello.mdx": __fd_glob_9, });
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Authentication (auth)
3
+ description: Email/password and GitHub OAuth authentication using Supabase Auth, implemented following the four-layer contract.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ The `auth` feature provides full authentication using **Supabase Auth**:
9
+
10
+ - Email + password sign-in and sign-up (with email verification)
11
+ - GitHub OAuth sign-in
12
+ - Session management via `@supabase/ssr` (httpOnly cookies, auto-refresh)
13
+ - Route protection via `(protected)` route group + server-side session check
14
+
15
+ UI components: **shadcn/ui** blocks (`login-03`, `signup-03`, `dashboard-01`)
16
+ Forms: **react-hook-form + zod** (all multi-field forms use this pattern)
17
+
18
+ ---
19
+
20
+ ## Architecture: Four-Layer Contract
21
+
22
+ ```
23
+ core/types/auth.ts ← Domain types (AuthUser, AuthResult, ActionResult)
24
+ core/repositories/IAuthRepository.ts ← Interface (no Supabase types)
25
+ infra/db/SupabaseAuthRepository.ts ← Implementation (Supabase SDK)
26
+ infra/providers.ts ← DI factory: getAuthRepository()
27
+ features/auth/__contract__.ts ← Public API types
28
+ features/auth/actions.ts ← Server Actions ('use server')
29
+ features/auth/index.ts ← Barrel export
30
+ ```
31
+
32
+ ### Why the interface layer?
33
+
34
+ `IAuthRepository` decouples features from the Supabase SDK. To swap to Auth.js:
35
+ 1. Implement `IAuthRepository` with Auth.js
36
+ 2. Update `infra/providers.ts` to return the new implementation
37
+ 3. `features/auth/actions.ts` is unchanged — zero feature churn
38
+
39
+ ---
40
+
41
+ ## Server Actions
42
+
43
+ Import from the feature barrel:
44
+
45
+ ```ts
46
+ import { signIn, signUp, signOut, signInWithGithub } from '@/features/auth'
47
+ ```
48
+
49
+ | Action | Returns | Side effect |
50
+ |--------|---------|------------|
51
+ | `signIn(prevState, formData)` | `ActionResult` | Redirects to `/${locale}/dashboard` on success |
52
+ | `signUp(prevState, formData)` | `ActionResult<{ success: true; email: string }>` | No redirect — waits for email verification |
53
+ | `signOut()` | `void` | Clears session, redirects to `/${locale}/login` |
54
+ | `signInWithGithub()` | `ActionResult<{ url: string }>` | Returns OAuth URL — caller does `router.push(url)` |
55
+
56
+ All actions use `useActionState` on the client side:
57
+
58
+ ```tsx
59
+ const [state, formAction, isPending] = useActionState(signIn, null)
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Route Structure
65
+
66
+ ```
67
+ src/app/
68
+ [locale]/
69
+ (auth)/ ← Unauthenticated pages (no layout)
70
+ login/page.tsx
71
+ signup/page.tsx
72
+ (protected)/ ← Authenticated pages
73
+ layout.tsx ← Server: checks session, redirects to /login if missing
74
+ dashboard/page.tsx
75
+ auth/
76
+ callback/route.ts ← OAuth + email verification callback (no locale prefix)
77
+ ```
78
+
79
+ ### Why no locale prefix on `/auth/callback`?
80
+
81
+ Supabase callbacks must be registered with a fixed URL. Using `/auth/callback` (without `[locale]`)
82
+ avoids registering multiple locale variants in the Supabase dashboard.
83
+ After a successful callback, the app redirects to `/en/dashboard` (default locale).
84
+
85
+ ---
86
+
87
+ ## Route Protection
88
+
89
+ Protection happens at the **layout level** (server-side), not middleware-only:
90
+
91
+ ```ts
92
+ // (protected)/layout.tsx
93
+ const { data: { user } } = await supabase.auth.getUser()
94
+ if (!user) redirect(`/${locale}/login`)
95
+ ```
96
+
97
+ `getUser()` is used (not `getSession()`) because it validates the JWT server-side,
98
+ preventing forged tokens.
99
+
100
+ Middleware additionally:
101
+ 1. Refreshes the Supabase session token on every request
102
+ 2. Redirects authenticated users away from `/(auth)` pages to `/dashboard`
103
+
104
+ ---
105
+
106
+ ## Zod Schemas
107
+
108
+ Defined in `src/lib/validations/auth.ts` — shared between Server Actions and UI:
109
+
110
+ ```ts
111
+ import { signInSchema, signUpSchema } from '@/lib/validations/auth'
112
+ ```
113
+
114
+ | Schema | Fields | Rules |
115
+ |--------|--------|-------|
116
+ | `signInSchema` | `email`, `password` | email format, min 8 chars |
117
+ | `signUpSchema` | `email`, `password`, `confirmPassword` | above + passwords match |
118
+
119
+ ---
120
+
121
+ ## Setup Requirements
122
+
123
+ Before auth works, configure Supabase:
124
+
125
+ 1. **GitHub OAuth** — Supabase Dashboard → Authentication → Providers → GitHub
126
+ 2. **Redirect URL** — Supabase Dashboard → Authentication → URL Configuration → add `http://localhost:3000/auth/callback`
127
+
128
+ See the template `README.md` for the full step-by-step guide.
129
+
130
+ ---
131
+
132
+ ## Non-Goals
133
+
134
+ This feature intentionally excludes:
135
+ - Password reset / forgot password
136
+ - User profile / avatar / account settings
137
+ - Role-based access control (RBAC)
138
+ - Magic link / OTP / phone login
139
+ - Custom email templates
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/[locale]/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -6,5 +6,33 @@
6
6
  "hello": {
7
7
  "title": "Hello Feature",
8
8
  "greeting": "Hello, {name}!"
9
+ },
10
+ "auth": {
11
+ "loginTitle": "Welcome back",
12
+ "loginSubtitle": "Sign in to your account",
13
+ "signupTitle": "Create an account",
14
+ "signupSubtitle": "Enter your details to get started",
15
+ "emailLabel": "Email",
16
+ "emailPlaceholder": "you@example.com",
17
+ "emailHelperText": "Use a real email address — you'll need to verify it after sign up.",
18
+ "passwordLabel": "Password",
19
+ "passwordPlaceholder": "••••••••",
20
+ "confirmPasswordLabel": "Confirm password",
21
+ "confirmPasswordPlaceholder": "••••••••",
22
+ "signInButton": "Sign in",
23
+ "signUpButton": "Create account",
24
+ "signOutButton": "Sign out",
25
+ "githubButton": "Sign in with GitHub",
26
+ "noAccountText": "Don't have an account?",
27
+ "signUpLink": "Sign up",
28
+ "hasAccountText": "Already have an account?",
29
+ "signInLink": "Sign in",
30
+ "orContinueWith": "Or continue with",
31
+ "verifyEmailTitle": "Check your email",
32
+ "verifyEmailMessage": "We sent a verification link to {email}. Click the link to complete your sign up.",
33
+ "dashboardTitle": "Dashboard",
34
+ "dashboardWelcome": "Welcome, {email}",
35
+ "errorInvalidCredentials": "Invalid email or password.",
36
+ "errorGeneric": "Something went wrong. Please try again."
9
37
  }
10
38
  }
@@ -6,5 +6,33 @@
6
6
  "hello": {
7
7
  "title": "hello.title",
8
8
  "greeting": "hello.greeting"
9
+ },
10
+ "auth": {
11
+ "loginTitle": "auth.loginTitle",
12
+ "loginSubtitle": "auth.loginSubtitle",
13
+ "signupTitle": "auth.signupTitle",
14
+ "signupSubtitle": "auth.signupSubtitle",
15
+ "emailLabel": "auth.emailLabel",
16
+ "emailPlaceholder": "auth.emailPlaceholder",
17
+ "emailHelperText": "auth.emailHelperText",
18
+ "passwordLabel": "auth.passwordLabel",
19
+ "passwordPlaceholder": "auth.passwordPlaceholder",
20
+ "confirmPasswordLabel": "auth.confirmPasswordLabel",
21
+ "confirmPasswordPlaceholder": "auth.confirmPasswordPlaceholder",
22
+ "signInButton": "auth.signInButton",
23
+ "signUpButton": "auth.signUpButton",
24
+ "signOutButton": "auth.signOutButton",
25
+ "githubButton": "auth.githubButton",
26
+ "noAccountText": "auth.noAccountText",
27
+ "signUpLink": "auth.signUpLink",
28
+ "hasAccountText": "auth.hasAccountText",
29
+ "signInLink": "auth.signInLink",
30
+ "orContinueWith": "auth.orContinueWith",
31
+ "verifyEmailTitle": "auth.verifyEmailTitle",
32
+ "verifyEmailMessage": "auth.verifyEmailMessage",
33
+ "dashboardTitle": "auth.dashboardTitle",
34
+ "dashboardWelcome": "auth.dashboardWelcome",
35
+ "errorInvalidCredentials": "auth.errorInvalidCredentials",
36
+ "errorGeneric": "auth.errorGeneric"
9
37
  }
10
38
  }
@@ -1,23 +1,67 @@
1
1
  import createMiddleware from 'next-intl/middleware'
2
2
  import { defineRouting } from 'next-intl/routing'
3
3
  import { NextResponse, type NextRequest } from 'next/server'
4
+ import { createMiddlewareClient } from '@/infra/db/client'
5
+ import { defaultLocale, isLocale, locales } from '@/i18n/config'
4
6
 
5
7
  export const routing = defineRouting({
6
- locales: ['en', 'zh'],
7
- defaultLocale: 'en',
8
+ locales,
9
+ defaultLocale,
8
10
  })
9
11
 
10
12
  const handleI18nRouting = createMiddleware(routing)
11
13
 
12
- export default function middleware(request: NextRequest) {
13
- const segments = request.nextUrl.pathname.split('/').filter(Boolean)
14
+ function copyCookies(from: NextResponse, to: NextResponse) {
15
+ for (const cookie of from.cookies.getAll()) {
16
+ to.cookies.set(cookie)
17
+ }
18
+ }
19
+
20
+ export default async function middleware(request: NextRequest) {
21
+ const pathname = request.nextUrl.pathname
22
+
23
+ if (pathname === '/') {
24
+ const url = request.nextUrl.clone()
25
+ url.pathname = `/${defaultLocale}`
26
+ return NextResponse.redirect(url)
27
+ }
28
+
29
+ // Skip Supabase session refresh for the auth callback route
30
+ // (it's handled by the route handler itself)
31
+ if (pathname.startsWith('/auth/')) {
32
+ return NextResponse.next()
33
+ }
34
+
35
+ // 1. Let next-intl build the final response first.
36
+ // Supabase cookie refresh should write into this response to avoid cookie loss.
37
+ const response = handleI18nRouting(request)
38
+
39
+ // 2. Refresh the Supabase session (keeps token alive, writes updated cookie)
40
+ const { supabase } = createMiddlewareClient(request, response)
41
+ const {
42
+ data: { user },
43
+ } = await supabase.auth.getUser()
44
+
45
+ // 3. Redirect authenticated users away from auth pages (/(auth)/ routes)
46
+ const authPagePattern = /^\/[a-z]{2}\/(login|signup)(\/|$)/
47
+ if (user && authPagePattern.test(pathname)) {
48
+ const localeSegment = pathname.split('/')[1] ?? defaultLocale
49
+ const locale = isLocale(localeSegment) ? localeSegment : defaultLocale
50
+ const url = request.nextUrl.clone()
51
+ url.pathname = `/${locale}/dashboard`
52
+ const redirectResponse = NextResponse.redirect(url)
53
+ copyCookies(response, redirectResponse)
54
+ return redirectResponse
55
+ }
56
+
57
+ // 4. Normalize unknown locale-like prefixes: /fr/hello -> /en/hello
58
+ const segments = pathname.split('/').filter(Boolean)
14
59
  const firstSegment = segments[0]
15
60
 
16
61
  if (firstSegment !== undefined) {
17
62
  const isLocaleLike = /^[a-z]{2}(?:-[A-Z]{2})?$/.test(firstSegment)
18
- const isSupportedLocale = routing.locales.includes(firstSegment as 'en' | 'zh')
63
+ const isSupportedLocale = isLocale(firstSegment)
19
64
 
20
- // Normalize unknown locale-like prefixes: /fr/hello -> /en/hello.
21
65
  if (isLocaleLike && !isSupportedLocale) {
22
66
  const url = request.nextUrl.clone()
23
67
  url.pathname = `/${routing.defaultLocale}/${segments.slice(1).join('/')}`.replace(/\/$/, '')
@@ -25,10 +69,10 @@ export default function middleware(request: NextRequest) {
25
69
  }
26
70
  }
27
71
 
28
- return handleI18nRouting(request)
72
+ return response
29
73
  }
30
74
 
31
75
  export const config = {
32
- // Match all pathnames except Next.js internals and static files.
33
- matcher: ['/((?!_next|_vercel|.*\\..*).*)'],
76
+ // Match all pathnames except Next.js internals, static files, and auth callback.
77
+ matcher: ['/((?!_next|_vercel|auth/callback|.*\\..*).*)'],
34
78
  }
@@ -11,11 +11,23 @@
11
11
  "check-types": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
+ "@base-ui/react": "^1.5.0",
15
+ "@hookform/resolvers": "^5.4.0",
14
16
  "@supabase/ssr": "^0.10.3",
17
+ "class-variance-authority": "^0.7.1",
18
+ "clsx": "^2.1.1",
19
+ "lucide-react": "^1.17.0",
15
20
  "next": "16",
16
21
  "next-intl": "^4.13.0",
22
+ "next-themes": "^0.4.6",
17
23
  "react": "19",
18
- "react-dom": "19"
24
+ "react-dom": "19",
25
+ "react-hook-form": "^7.77.0",
26
+ "shadcn": "^4.10.0",
27
+ "sonner": "^2.0.7",
28
+ "tailwind-merge": "^3.6.0",
29
+ "tw-animate-css": "^1.4.0",
30
+ "zod": "^4.4.3"
19
31
  },
20
32
  "devDependencies": {
21
33
  "@cogito.ai/eslint-config": "workspace:*",