docs: 同步 AGENTS.md — 单 owner 模型 + 强制 migration workflow + CLI 文档

This commit is contained in:
2026-03-31 19:01:54 +08:00
parent d3f2088fc8
commit 5e65c37a26
2 changed files with 30 additions and 11 deletions
+7 -4
View File
@@ -41,12 +41,14 @@ bun run compile # Compile server to standalone binary
bun run fix # Lint + format (Biome auto-fix) bun run fix # Lint + format (Biome auto-fix)
bun run typecheck # TypeScript check across monorepo bun run typecheck # TypeScript check across monorepo
# Database (in apps/server) # Database (in apps/server) — ALWAYS use migration workflow, never db:push
bun run db:generate # Generate migrations from schema bun run db:generate # Generate migrations from schema changes
bun run db:migrate # Run migrations bun run db:migrate # Run pending migrations
bun run db:push # Push schema (dev only)
bun run db:studio # Open Drizzle Studio bun run db:studio # Open Drizzle Studio
# Server CLI (in apps/server)
bun run cli auth reset-password # Reset owner password
# Testing (not yet configured) # Testing (not yet configured)
bun test path/to/test.ts # Run single test file bun test path/to/test.ts # Run single test file
``` ```
@@ -93,6 +95,7 @@ Biome auto-organizes. Order: 1) External packages → 2) Internal `@/*` aliases
- **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package) - **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package)
- **Relations**: Defined via `defineRelations()` — RQBv2 object syntax - **Relations**: Defined via `defineRelations()` — RQBv2 object syntax
- **Query style**: `db.query.tableName.findMany({ orderBy: { createdAt: 'desc' }, where: { id: 1 } })` - **Query style**: `db.query.tableName.findMany({ orderBy: { createdAt: 'desc' }, where: { id: 1 } })`
- **Migration workflow**: Always `db:generate``db:migrate`. **Never** use `db:push`.
## Environment Variables ## Environment Variables
+23 -7
View File
@@ -11,7 +11,8 @@ TanStack Start fullstack web app with ORPC (contract-first RPC) and shadcn/ui.
- **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2) - **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2)
- **State**: TanStack Query v5 (with MutationCache auto-invalidation) - **State**: TanStack Query v5 (with MutationCache auto-invalidation)
- **RPC**: ORPC (contract-first, type-safe) - **RPC**: ORPC (contract-first, type-safe)
- **Auth**: Better Auth (email+password, self-hosted) - **Auth**: Better Auth (email+password, single-owner, self-hosted)
- **CLI**: citty (server-side admin commands)
- **DnD**: @dnd-kit/react + @dnd-kit/helpers (`move()` for sortable) - **DnD**: @dnd-kit/react + @dnd-kit/helpers (`move()` for sortable)
- **Virtualization**: @tanstack/react-virtual (`useVirtualizer`) - **Virtualization**: @tanstack/react-virtual (`useVirtualizer`)
- **Build**: Vite + Nitro - **Build**: Vite + Nitro
@@ -24,10 +25,11 @@ TanStack Start fullstack web app with ORPC (contract-first RPC) and shadcn/ui.
/ → Dashboard homepage (bookmark display, daily use) / → Dashboard homepage (bookmark display, daily use)
/admin → Admin panel overview (module cards) /admin → Admin panel overview (module cards)
/admin/bookmarks → Bookmark management (CRUD, DnD, Dialog forms) /admin/bookmarks → Bookmark management (CRUD, DnD, Dialog forms)
/loginLogin page /setupOne-time owner setup (first visit only, redirects to /login after)
/signupSignup page /login Login page (redirects to /setup if no owner exists)
``` ```
- **Single-owner model**: Kairos is a self-hosted Life OS. Only ONE user (the owner) exists. There is NO registration page — `/setup` is a one-time wizard shown on first visit.
- **Display pages** (`/`): Clean, no management UI. What users see daily. - **Display pages** (`/`): Clean, no management UI. What users see daily.
- **Admin pages** (`/admin/*`): Full CRUD, management, configuration. Sidebar navigation. - **Admin pages** (`/admin/*`): Full CRUD, management, configuration. Sidebar navigation.
- All authenticated routes under `_protected` layout (auth guard → redirect to `/login`). - All authenticated routes under `_protected` layout (auth guard → redirect to `/login`).
@@ -63,6 +65,9 @@ src/
├── modules/ ├── modules/
│ ├── registry.ts # ModuleMetadata interface + modules[] │ ├── registry.ts # ModuleMetadata interface + modules[]
│ └── bookmarks/ # Bookmarks module (schema, contract, router, components/) │ └── bookmarks/ # Bookmarks module (schema, contract, router, components/)
├── cli/
│ ├── index.ts # citty CLI entrypoint (bun run cli ...)
│ └── commands/auth.ts # auth reset-password command
├── routes/ # TanStack Router file routes ├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Root layout (HTML shell, Toaster) │ ├── __root.tsx # Root layout (HTML shell, Toaster)
│ ├── _protected.tsx # Auth guard layout │ ├── _protected.tsx # Auth guard layout
@@ -70,7 +75,7 @@ src/
│ │ ├── index.tsx # Dashboard homepage │ │ ├── index.tsx # Dashboard homepage
│ │ ├── admin.tsx # Admin layout (SidebarProvider) │ │ ├── admin.tsx # Admin layout (SidebarProvider)
│ │ └── admin/bookmarks.tsx │ │ └── admin/bookmarks.tsx
│ ├── login.tsx, signup.tsx │ ├── login.tsx, setup.tsx
│ └── api/ # $.ts (OpenAPI), auth.$.ts, health.ts, rpc.$.ts │ └── api/ # $.ts (OpenAPI), auth.$.ts, health.ts, rpc.$.ts
├── server/ ├── server/
│ ├── api/ │ ├── api/
@@ -81,7 +86,7 @@ src/
│ │ ├── context.ts # BaseContext type │ │ ├── context.ts # BaseContext type
│ │ ├── server.ts # `os = implement(contract).$context<BaseContext>()` │ │ ├── server.ts # `os = implement(contract).$context<BaseContext>()`
│ │ └── types.ts # RouterClient type export │ │ └── types.ts # RouterClient type export
│ ├── auth/ # Better Auth (schema, instance, client, getSession) │ ├── auth/ # Better Auth (schema, instance, client, getSession, checkInitialized)
│ └── db/ # Drizzle (fields, relations, singleton instance) │ └── db/ # Drizzle (fields, relations, singleton instance)
├── env.ts # @t3-oss/env-core validation ├── env.ts # @t3-oss/env-core validation
├── router.tsx # TanStack Router + QueryClient + MutationCache ├── router.tsx # TanStack Router + QueryClient + MutationCache
@@ -247,6 +252,8 @@ toast.error('操作失败')
- **Table naming**: No `Table` suffix — `user`, `category`, `bookmark` - **Table naming**: No `Table` suffix — `user`, `category`, `bookmark`
- **DB instance**: Module-level singleton `export const db = drizzle(...)` (NOT factory pattern) - **DB instance**: Module-level singleton `export const db = drizzle(...)` (NOT factory pattern)
- **Shared fields**: Use `...generatedFields` spread for id/createdAt/updatedAt - **Shared fields**: Use `...generatedFields` spread for id/createdAt/updatedAt
- **Migration workflow**: Always `db:generate``db:migrate`. **Never** use `db:push`.
- **Path alias exception**: Files in the Drizzle schema chain (`db/schema/index.ts`, module `schema.ts`) MUST use relative imports — `drizzle-kit` does not resolve `@/*` aliases.
```typescript ```typescript
// Schema — use generatedFields spread // Schema — use generatedFields spread
@@ -264,14 +271,20 @@ export const relations = defineRelations(schema, (r) => ({
})) }))
``` ```
## Auth (Better Auth) ## Auth (Better Auth — Single-Owner Model)
Kairos is a self-hosted single-user app. There is NO public registration. The first visit triggers a one-time setup wizard (`/setup`), and all subsequent signup attempts are blocked at the database level.
- **Instance**: `src/server/auth/index.ts``betterAuth()` with `drizzleAdapter(db, { provider: 'pg', schema })` - **Instance**: `src/server/auth/index.ts``betterAuth()` with `drizzleAdapter(db, { provider: 'pg', schema })`
- **Signup blocking**: `databaseHooks.user.create.before` checks if a user already exists → throws `APIError('FORBIDDEN')` if so
- **Client**: `src/server/auth/client.ts``createAuthClient()` for React - **Client**: `src/server/auth/client.ts``createAuthClient()` for React
- **Server function**: `src/server/auth/functions.ts``getSession()` via `createServerFn` - **Server functions**: `src/server/auth/functions.ts`:
- `getSession()` — get current session via `createServerFn`
- `checkInitialized()` — check if owner account exists (used by `/setup` and `/login` routes)
- **Auth tables**: Use `text` IDs (Better Auth manages its own IDs), NOT project's UUID v7 - **Auth tables**: Use `text` IDs (Better Auth manages its own IDs), NOT project's UUID v7
- **Route guard**: `beforeLoad` in `_protected.tsx` calls `getSession()` → redirect to `/login` - **Route guard**: `beforeLoad` in `_protected.tsx` calls `getSession()` → redirect to `/login`
- **ORPC middleware**: `authMiddleware` calls `auth.api.getSession({ headers })` → injects `context.user` - **ORPC middleware**: `authMiddleware` calls `auth.api.getSession({ headers })` → injects `context.user`
- **Password reset**: Via server CLI only (`bun run cli auth reset-password`) — no web-based password recovery
## Critical Rules ## Critical Rules
@@ -294,3 +307,6 @@ export const relations = defineRelations(schema, (r) => ({
- Pass `schema` to `drizzle()` constructor (only `relations` needed in RQBv2) - Pass `schema` to `drizzle()` constructor (only `relations` needed in RQBv2)
- Add `Table` suffix to Drizzle table exports - Add `Table` suffix to Drizzle table exports
- Use `useRef` for scroll elements inside Dialog/conditional rendering - Use `useRef` for scroll elements inside Dialog/conditional rendering
- Use `db:push` — always use `db:generate``db:migrate`
- Use `@/*` aliases in Drizzle schema files (drizzle-kit can't resolve them)
- Add registration/signup functionality (single-owner model, enforced by `databaseHooks`)