@helloleo/plugins 0.2.11 → 0.2.13
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 +20 -3
- package/dist/templates/tanstack-start-2026-07-30/components.json +22 -0
- package/dist/templates/tanstack-start-2026-07-30/drizzle/0000_tidy_kang.sql +5 -0
- package/dist/templates/tanstack-start-2026-07-30/drizzle/meta/0000_snapshot.json +50 -0
- package/dist/templates/tanstack-start-2026-07-30/drizzle/meta/_journal.json +13 -0
- package/dist/templates/tanstack-start-2026-07-30/drizzle.config.ts +18 -0
- package/dist/templates/tanstack-start-2026-07-30/package.json +103 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/error-component.tsx +47 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/accordion.tsx +51 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/alert-dialog.tsx +115 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/alert.tsx +49 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/aspect-ratio.tsx +5 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/avatar.tsx +47 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/badge.tsx +32 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/breadcrumb.tsx +101 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/button.tsx +49 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/calendar.tsx +177 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/card.tsx +55 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/carousel.tsx +240 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/chart.tsx +371 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/checkbox.tsx +26 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/collapsible.tsx +11 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/command.tsx +143 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/context-menu.tsx +187 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/dialog.tsx +104 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/drawer.tsx +98 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/dropdown-menu.tsx +188 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/form.tsx +171 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/hover-card.tsx +27 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/input-otp.tsx +69 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/input.tsx +22 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/label.tsx +21 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/menubar.tsx +229 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/navigation-menu.tsx +120 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/pagination.tsx +98 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/popover.tsx +31 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/progress.tsx +25 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/radio-group.tsx +36 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/resizable.tsx +37 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/scroll-area.tsx +44 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/select.tsx +152 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/separator.tsx +24 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/sheet.tsx +122 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/sidebar.tsx +744 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/skeleton.tsx +7 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/slider.tsx +23 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/sonner.tsx +23 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/switch.tsx +27 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/table.tsx +94 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/tabs.tsx +53 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/textarea.tsx +21 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/toggle-group.tsx +57 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/toggle.tsx +42 -0
- package/dist/templates/tanstack-start-2026-07-30/src/components/ui/tooltip.tsx +32 -0
- package/dist/templates/tanstack-start-2026-07-30/src/db/index.ts +9 -0
- package/dist/templates/tanstack-start-2026-07-30/src/db/schema.ts +18 -0
- package/dist/templates/tanstack-start-2026-07-30/src/global.d.ts +3 -0
- package/dist/templates/tanstack-start-2026-07-30/src/hooks/use-mobile.tsx +19 -0
- package/dist/templates/tanstack-start-2026-07-30/src/lib/proxy.ts +82 -0
- package/dist/templates/tanstack-start-2026-07-30/src/lib/utils.ts +6 -0
- package/dist/templates/tanstack-start-2026-07-30/src/routeTree.gen.ts +86 -0
- package/dist/templates/tanstack-start-2026-07-30/src/router.tsx +37 -0
- package/dist/templates/tanstack-start-2026-07-30/src/routes/README.md +94 -0
- package/dist/templates/tanstack-start-2026-07-30/src/routes/__root.tsx +65 -0
- package/dist/templates/tanstack-start-2026-07-30/src/routes/api/health.ts +81 -0
- package/dist/templates/tanstack-start-2026-07-30/src/routes/index.tsx +33 -0
- package/dist/templates/tanstack-start-2026-07-30/src/server.ts +32 -0
- package/dist/templates/tanstack-start-2026-07-30/src/styles.css +214 -0
- package/dist/templates/tanstack-start-2026-07-30/tsconfig.json +27 -0
- package/dist/templates/tanstack-start-2026-07-30/vite.config.ts +17 -0
- package/package.json +1 -1
- package/src/templates/tanstack-start-2026-07-30/components.json +22 -0
- package/src/templates/tanstack-start-2026-07-30/drizzle/0000_tidy_kang.sql +5 -0
- package/src/templates/tanstack-start-2026-07-30/drizzle/meta/0000_snapshot.json +50 -0
- package/src/templates/tanstack-start-2026-07-30/drizzle/meta/_journal.json +13 -0
- package/src/templates/tanstack-start-2026-07-30/drizzle.config.ts +18 -0
- package/src/templates/tanstack-start-2026-07-30/package.json +103 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/error-component.tsx +47 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/accordion.tsx +51 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/alert-dialog.tsx +115 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/alert.tsx +49 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/avatar.tsx +47 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/badge.tsx +32 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/breadcrumb.tsx +101 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/button.tsx +49 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/calendar.tsx +177 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/card.tsx +55 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/carousel.tsx +240 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/chart.tsx +371 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/checkbox.tsx +26 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/collapsible.tsx +11 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/command.tsx +143 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/context-menu.tsx +187 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/dialog.tsx +104 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/drawer.tsx +98 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/dropdown-menu.tsx +188 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/form.tsx +171 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/hover-card.tsx +27 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/input-otp.tsx +69 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/input.tsx +22 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/label.tsx +21 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/menubar.tsx +229 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/navigation-menu.tsx +120 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/pagination.tsx +98 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/popover.tsx +31 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/progress.tsx +25 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/radio-group.tsx +36 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/resizable.tsx +37 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/scroll-area.tsx +44 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/select.tsx +152 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/separator.tsx +24 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/sheet.tsx +122 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/sidebar.tsx +744 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/skeleton.tsx +7 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/slider.tsx +23 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/sonner.tsx +23 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/switch.tsx +27 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/table.tsx +94 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/tabs.tsx +53 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/textarea.tsx +21 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/toggle-group.tsx +57 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/toggle.tsx +42 -0
- package/src/templates/tanstack-start-2026-07-30/src/components/ui/tooltip.tsx +32 -0
- package/src/templates/tanstack-start-2026-07-30/src/db/index.ts +9 -0
- package/src/templates/tanstack-start-2026-07-30/src/db/schema.ts +18 -0
- package/src/templates/tanstack-start-2026-07-30/src/global.d.ts +3 -0
- package/src/templates/tanstack-start-2026-07-30/src/hooks/use-mobile.tsx +19 -0
- package/src/templates/tanstack-start-2026-07-30/src/lib/proxy.ts +82 -0
- package/src/templates/tanstack-start-2026-07-30/src/lib/utils.ts +6 -0
- package/src/templates/tanstack-start-2026-07-30/src/routeTree.gen.ts +86 -0
- package/src/templates/tanstack-start-2026-07-30/src/router.tsx +37 -0
- package/src/templates/tanstack-start-2026-07-30/src/routes/README.md +94 -0
- package/src/templates/tanstack-start-2026-07-30/src/routes/__root.tsx +65 -0
- package/src/templates/tanstack-start-2026-07-30/src/routes/api/health.ts +81 -0
- package/src/templates/tanstack-start-2026-07-30/src/routes/index.tsx +33 -0
- package/src/templates/tanstack-start-2026-07-30/src/server.ts +32 -0
- package/src/templates/tanstack-start-2026-07-30/src/styles.css +214 -0
- package/src/templates/tanstack-start-2026-07-30/tsconfig.json +27 -0
- package/src/templates/tanstack-start-2026-07-30/vite.config.ts +17 -0
- package/src/tools/scaffold.ts +2 -2
- package/src/tools/setup-authentication/index.ts +29 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
TanStack Start uses file-based routing. Every `.ts` and `.tsx` file in this directory is a route. Do not create `src/pages/`, `app/layout.tsx`, or similar, those are Next.js / Remix conventions. The only root layout is `src/routes/__root.tsx`.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
| File | URL |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `index.tsx` | `/` |
|
|
10
|
+
| `about.tsx` | `/about` |
|
|
11
|
+
| `users/index.tsx` | `/users` |
|
|
12
|
+
| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) |
|
|
13
|
+
| `posts.$postId.tsx` | `/posts/:postId` (dots nest without folders) |
|
|
14
|
+
| `admin/route.tsx` | layout for every `/admin/*` page; must render `<Outlet />` |
|
|
15
|
+
| `admin_.login.tsx` | `/admin/login`, but outside the admin layout (trailing `_` escapes it) |
|
|
16
|
+
| `_auth.tsx` | pathless layout; wraps `_auth/*` files without adding a URL segment |
|
|
17
|
+
| `api/health.ts` | server route for defining API endpoints |
|
|
18
|
+
| `__root.tsx` | app shell, wraps every page; preserve `<Outlet />` |
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
- A layout route without `<Outlet />` renders its children nowhere.
|
|
23
|
+
- Do not `export default` route components, it breaks code splitting. Use a named function passed as `component:`.
|
|
24
|
+
- `routeTree.gen.ts` is auto-generated. Don't edit it by hand.
|
|
25
|
+
|
|
26
|
+
## API routes vs server functions
|
|
27
|
+
|
|
28
|
+
TanStack Start offers two ways to run server-side code:
|
|
29
|
+
|
|
30
|
+
- [Server routes](https://tanstack.com/start/latest/docs/framework/react/guide/server-routes) — files under `src/routes/api/` that export HTTP handlers.
|
|
31
|
+
- [Server functions](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) — `createServerFn()` calls colocated with components.
|
|
32
|
+
|
|
33
|
+
Prefer API routes. Create them under `src/routes/api/` following the same convention as `src/routes/api/health.ts` (one handler per HTTP method under `server.handlers`, returning `Response.json()`). This is more scalable and gives the user visually readable endpoints (`/api/todos`, `/api/todos/:id`) they can recognize in the network monitor. Server functions are served from generated URLs that read as random gibberish, which makes requests hard to identify and debug.
|
|
34
|
+
|
|
35
|
+
## Authenticated routes
|
|
36
|
+
|
|
37
|
+
Put protected pages inside a pathless `_authed` layout and keep login/signup
|
|
38
|
+
at the top level, outside it. Never special-case the login pathname inside
|
|
39
|
+
the layout guard.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
routes/
|
|
43
|
+
__root.tsx loads the session into router context (context.user)
|
|
44
|
+
_authed.tsx pathless guard — no URL segment of its own
|
|
45
|
+
_authed/
|
|
46
|
+
dashboard.tsx /dashboard (protected)
|
|
47
|
+
posts.tsx /posts (protected)
|
|
48
|
+
index.tsx / (public)
|
|
49
|
+
login.tsx /login (public)
|
|
50
|
+
logout.tsx /logout
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`_authed.tsx` does the guarding, all children inherit it:
|
|
54
|
+
|
|
55
|
+
```tsx _authed.tsx
|
|
56
|
+
export const Route = createFileRoute('/_authed')({
|
|
57
|
+
beforeLoad: ({ context }) => {
|
|
58
|
+
if (!context.user) {
|
|
59
|
+
throw redirect({ to: '/login' })
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The session is loaded once in `__root.tsx`'s `beforeLoad` and returned into
|
|
66
|
+
the router context so every route can read `context.user`. `getUser()` is
|
|
67
|
+
whatever your auth uses (e.g. verify a JWT from localStorage) and resolves to
|
|
68
|
+
the user or null:
|
|
69
|
+
|
|
70
|
+
```tsx __root.tsx
|
|
71
|
+
// (keep the existing head() and component, add this)
|
|
72
|
+
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
|
73
|
+
beforeLoad: async () => {
|
|
74
|
+
const user = await getUser()
|
|
75
|
+
return { user }
|
|
76
|
+
},
|
|
77
|
+
// ...head, component unchanged
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Components read it with `Route.useRouteContext()` (e.g. to swap a Login link
|
|
82
|
+
for a Logout button in the shell):
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
function Route() {
|
|
86
|
+
const { user } = Route.useRouteContext()
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<nav>
|
|
90
|
+
{user ? <Link to="/logout">Logout</Link> : <Link to="/login">Login</Link>}
|
|
91
|
+
</nav>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HeadContent,
|
|
3
|
+
Outlet,
|
|
4
|
+
Scripts,
|
|
5
|
+
createRootRouteWithContext,
|
|
6
|
+
} from '@tanstack/react-router'
|
|
7
|
+
import type { QueryClient } from '@tanstack/react-query'
|
|
8
|
+
import type { ReactNode } from 'react'
|
|
9
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
10
|
+
|
|
11
|
+
// Side-effect order matters: Tailwind needs to be linked into the SSR'd
|
|
12
|
+
// document. Importing the stylesheet as a URL and listing it in `head.links`
|
|
13
|
+
// is the Start-native way (a bare `import './styles.css'` only works client-side).
|
|
14
|
+
import appCss from '../styles.css?url'
|
|
15
|
+
|
|
16
|
+
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
|
17
|
+
head: () => ({
|
|
18
|
+
meta: [
|
|
19
|
+
{ charSet: 'utf-8' },
|
|
20
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
|
|
21
|
+
{ title: '{{LEO_NAME}}' },
|
|
22
|
+
{ name: 'description', content: '{{LEO_DESCRIPTION}}' },
|
|
23
|
+
{ property: 'og:title', content: '{{LEO_NAME}}' },
|
|
24
|
+
{ property: 'og:description', content: '{{LEO_DESCRIPTION}}' },
|
|
25
|
+
{ property: 'og:type', content: 'website' },
|
|
26
|
+
],
|
|
27
|
+
links: [
|
|
28
|
+
{ rel: 'stylesheet', href: appCss },
|
|
29
|
+
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
|
30
|
+
{
|
|
31
|
+
rel: 'preconnect',
|
|
32
|
+
href: 'https://fonts.gstatic.com',
|
|
33
|
+
crossOrigin: 'anonymous',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
rel: 'stylesheet',
|
|
37
|
+
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
component: RootComponent,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function RootComponent() {
|
|
45
|
+
return (
|
|
46
|
+
<RootDocument>
|
|
47
|
+
<Outlet />
|
|
48
|
+
</RootDocument>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|
53
|
+
return (
|
|
54
|
+
<html lang="en">
|
|
55
|
+
<head>
|
|
56
|
+
<HeadContent />
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
{children}
|
|
60
|
+
<Toaster richColors position="top-right" />
|
|
61
|
+
<Scripts />
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { db } from '#/db'
|
|
3
|
+
import { entries } from '#/db/schema'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server route example: GET /api/health. Use this file as the base for
|
|
7
|
+
* building JSON APIs (CRUD endpoints, webhooks) with TanStack Start
|
|
8
|
+
* server routes:
|
|
9
|
+
* https://tanstack.com/start/latest/docs/framework/react/guide/server-routes
|
|
10
|
+
*
|
|
11
|
+
* How server routes work:
|
|
12
|
+
* - Files under src/routes/ can export server handlers. The URL matches
|
|
13
|
+
* the file path: src/routes/api/health.ts serves /api/health.
|
|
14
|
+
* - Add one handler per HTTP method under server.handlers (GET, POST,
|
|
15
|
+
* PUT, PATCH, DELETE). A handler receives ({ request, params }) and
|
|
16
|
+
* returns a Response, usually Response.json().
|
|
17
|
+
* - Dynamic segments use $: src/routes/api/todos/$id.ts serves
|
|
18
|
+
* /api/todos/:id and the handler reads params.id.
|
|
19
|
+
* - Read the request body with await request.json() and validate it
|
|
20
|
+
* before use. Return proper status codes (201 created, 404 not found).
|
|
21
|
+
* - Query the database with `import { db } from '#/db'` INSIDE the
|
|
22
|
+
* handler, never at module scope (the binding only exists per request).
|
|
23
|
+
*/
|
|
24
|
+
export const Route = createFileRoute('/api/health')({
|
|
25
|
+
server: {
|
|
26
|
+
handlers: {
|
|
27
|
+
GET: async () => {
|
|
28
|
+
const startedAt = Date.now()
|
|
29
|
+
|
|
30
|
+
const server = {
|
|
31
|
+
status: 'ok' as const,
|
|
32
|
+
uptimeSeconds: Math.round(
|
|
33
|
+
typeof process !== 'undefined' &&
|
|
34
|
+
typeof process.uptime === 'function'
|
|
35
|
+
? process.uptime()
|
|
36
|
+
: 0,
|
|
37
|
+
),
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
runtime: 'cloudflare-workers',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Database health: lightweight query against a known table
|
|
43
|
+
let database: {
|
|
44
|
+
status: 'ok' | 'error'
|
|
45
|
+
latencyMs: number
|
|
46
|
+
error: string | null
|
|
47
|
+
}
|
|
48
|
+
const dbStart = Date.now()
|
|
49
|
+
try {
|
|
50
|
+
await db.select({ id: entries.id }).from(entries).limit(1)
|
|
51
|
+
database = {
|
|
52
|
+
status: 'ok',
|
|
53
|
+
latencyMs: Date.now() - dbStart,
|
|
54
|
+
error: null,
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
database = {
|
|
58
|
+
status: 'error',
|
|
59
|
+
latencyMs: Date.now() - dbStart,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const overall =
|
|
65
|
+
server.status === 'ok' && database.status === 'ok'
|
|
66
|
+
? 'ok'
|
|
67
|
+
: 'degraded'
|
|
68
|
+
|
|
69
|
+
return Response.json(
|
|
70
|
+
{
|
|
71
|
+
status: overall,
|
|
72
|
+
server,
|
|
73
|
+
database,
|
|
74
|
+
checkedInMs: Date.now() - startedAt,
|
|
75
|
+
},
|
|
76
|
+
{ status: overall === 'ok' ? 200 : 503 },
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
3
|
+
|
|
4
|
+
// Server functions run only on the server (dev: node + cf-shim; prod: the
|
|
5
|
+
// worker). This is the seam where you read your bindings — import { db } from
|
|
6
|
+
// '#/db' and query Drizzle, or import { kv, storage } from '@helloleo/runtime'.
|
|
7
|
+
// Keep binding access INSIDE the handler, never at module scope.
|
|
8
|
+
const getMessage = createServerFn({ method: 'GET' }).handler(() => {
|
|
9
|
+
return { message: 'Hello from the server' }
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute('/')({
|
|
13
|
+
component: Home,
|
|
14
|
+
loader: () => getMessage(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Do not `export default function Home()`. This breaks the default
|
|
19
|
+
* codesplitting by Tanstack Router.
|
|
20
|
+
*/
|
|
21
|
+
function Home() {
|
|
22
|
+
const { message } = Route.useLoaderData()
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-8">
|
|
26
|
+
<h1 className="text-4xl font-bold">Welcome to HelloLeo</h1>
|
|
27
|
+
<p className="mt-4 text-lg">{message}</p>
|
|
28
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
29
|
+
Edit <code>src/routes/index.tsx</code> to get started.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Cloudflare Worker entry (the Worker `main`, configured by @helloleo/vite-config).
|
|
2
|
+
// Wraps the TanStack Start server entry so an SSR failure renders a clean 500
|
|
3
|
+
// instead of a raw stack.
|
|
4
|
+
type ServerEntry = {
|
|
5
|
+
fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let serverEntryPromise: Promise<ServerEntry> | undefined
|
|
9
|
+
|
|
10
|
+
async function getServerEntry(): Promise<ServerEntry> {
|
|
11
|
+
if (!serverEntryPromise) {
|
|
12
|
+
serverEntryPromise = import('@tanstack/react-start/server-entry').then(
|
|
13
|
+
(m) => ((m as { default?: ServerEntry }).default ?? m) as ServerEntry,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
return serverEntryPromise
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
async fetch(request: Request, env: unknown, ctx: unknown) {
|
|
21
|
+
try {
|
|
22
|
+
const handler = await getServerEntry()
|
|
23
|
+
return await handler.fetch(request, env, ctx)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error)
|
|
26
|
+
return new Response('<h1>Something went wrong</h1>', {
|
|
27
|
+
status: 500,
|
|
28
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HelloLeo Design System -- READ BEFORE STYLING.
|
|
3
|
+
*
|
|
4
|
+
* Design tokens are the single source of truth and power the no-code Design
|
|
5
|
+
* panel (the user edits colors / fonts / radius visually). Conventions:
|
|
6
|
+
* - Raw tokens live in :root (light) and .dark (dark). The @media
|
|
7
|
+
* (prefers-color-scheme: dark) block mirrors .dark so apps follow the OS
|
|
8
|
+
* by default; adding the `dark` class still forces dark explicitly.
|
|
9
|
+
* - @theme inline maps each raw token to a Tailwind utility
|
|
10
|
+
* (bg-background, text-foreground, rounded-lg, ...). Edit the raw token,
|
|
11
|
+
* not the component class. Never hardcode #hex in components.
|
|
12
|
+
* - When you add a color token, add it to :root, .dark, AND the media block,
|
|
13
|
+
* or one theme breaks.
|
|
14
|
+
*
|
|
15
|
+
* UI primitives live in src/components/ui (shadcn/ui) and consume these
|
|
16
|
+
* tokens. One responsibility per file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@import "tailwindcss" source(none);
|
|
20
|
+
@source "../src";
|
|
21
|
+
@import "tw-animate-css";
|
|
22
|
+
|
|
23
|
+
@custom-variant dark (&:is(.dark *));
|
|
24
|
+
|
|
25
|
+
@theme inline {
|
|
26
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
27
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
28
|
+
--radius-lg: var(--radius);
|
|
29
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
30
|
+
--color-background: var(--background);
|
|
31
|
+
--color-foreground: var(--foreground);
|
|
32
|
+
--color-card: var(--card);
|
|
33
|
+
--color-card-foreground: var(--card-foreground);
|
|
34
|
+
--color-popover: var(--popover);
|
|
35
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
36
|
+
--color-primary: var(--primary);
|
|
37
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
38
|
+
--color-secondary: var(--secondary);
|
|
39
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
40
|
+
--color-muted: var(--muted);
|
|
41
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
42
|
+
--color-accent: var(--accent);
|
|
43
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
44
|
+
--color-destructive: var(--destructive);
|
|
45
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
46
|
+
--color-border: var(--border);
|
|
47
|
+
--color-input: var(--input);
|
|
48
|
+
--color-ring: var(--ring);
|
|
49
|
+
--color-chart-1: var(--chart-1);
|
|
50
|
+
--color-chart-2: var(--chart-2);
|
|
51
|
+
--color-chart-3: var(--chart-3);
|
|
52
|
+
--color-chart-4: var(--chart-4);
|
|
53
|
+
--color-chart-5: var(--chart-5);
|
|
54
|
+
--color-sidebar: var(--sidebar);
|
|
55
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
56
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
57
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
58
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
59
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
60
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
61
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
62
|
+
--font-sans: "Inter", system-ui, sans-serif;
|
|
63
|
+
--font-display: "Space Grotesk", "Inter", sans-serif;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:root {
|
|
67
|
+
--radius: 0.625rem;
|
|
68
|
+
--background: oklch(1 0 0);
|
|
69
|
+
--foreground: oklch(0.145 0 0);
|
|
70
|
+
--card: oklch(1 0 0);
|
|
71
|
+
--card-foreground: oklch(0.145 0 0);
|
|
72
|
+
--popover: oklch(1 0 0);
|
|
73
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
74
|
+
--primary: oklch(0.205 0 0);
|
|
75
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
76
|
+
--secondary: oklch(0.97 0 0);
|
|
77
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
78
|
+
--muted: oklch(0.97 0 0);
|
|
79
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
80
|
+
--accent: oklch(0.97 0 0);
|
|
81
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
82
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
83
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
84
|
+
--border: oklch(0.922 0 0);
|
|
85
|
+
--input: oklch(0.922 0 0);
|
|
86
|
+
--ring: oklch(0.708 0 0);
|
|
87
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
88
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
89
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
90
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
91
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
92
|
+
--sidebar: oklch(0.985 0 0);
|
|
93
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
94
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
95
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
96
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
97
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
98
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
99
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Dark palette -- kept identical in .dark (explicit) and the media block
|
|
103
|
+
* (system default). Edit BOTH when changing a token. */
|
|
104
|
+
.dark {
|
|
105
|
+
--background: oklch(0.145 0 0);
|
|
106
|
+
--foreground: oklch(0.985 0 0);
|
|
107
|
+
--card: oklch(0.205 0 0);
|
|
108
|
+
--card-foreground: oklch(0.985 0 0);
|
|
109
|
+
--popover: oklch(0.205 0 0);
|
|
110
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
111
|
+
--primary: oklch(0.922 0 0);
|
|
112
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
113
|
+
--secondary: oklch(0.269 0 0);
|
|
114
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
115
|
+
--muted: oklch(0.269 0 0);
|
|
116
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
117
|
+
--accent: oklch(0.269 0 0);
|
|
118
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
119
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
120
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
121
|
+
--border: oklch(1 0 0 / 10%);
|
|
122
|
+
--input: oklch(1 0 0 / 15%);
|
|
123
|
+
--ring: oklch(0.556 0 0);
|
|
124
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
125
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
126
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
127
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
128
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
129
|
+
--sidebar: oklch(0.205 0 0);
|
|
130
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
131
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
132
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
133
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
134
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
135
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
136
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (prefers-color-scheme: dark) {
|
|
140
|
+
:root {
|
|
141
|
+
--background: oklch(0.145 0 0);
|
|
142
|
+
--foreground: oklch(0.985 0 0);
|
|
143
|
+
--card: oklch(0.205 0 0);
|
|
144
|
+
--card-foreground: oklch(0.985 0 0);
|
|
145
|
+
--popover: oklch(0.205 0 0);
|
|
146
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
147
|
+
--primary: oklch(0.922 0 0);
|
|
148
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
149
|
+
--secondary: oklch(0.269 0 0);
|
|
150
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
151
|
+
--muted: oklch(0.269 0 0);
|
|
152
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
153
|
+
--accent: oklch(0.269 0 0);
|
|
154
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
155
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
156
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
157
|
+
--border: oklch(1 0 0 / 10%);
|
|
158
|
+
--input: oklch(1 0 0 / 15%);
|
|
159
|
+
--ring: oklch(0.556 0 0);
|
|
160
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
161
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
162
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
163
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
164
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
165
|
+
--sidebar: oklch(0.205 0 0);
|
|
166
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
167
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
168
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
169
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
170
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
171
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
172
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@layer base {
|
|
177
|
+
* {
|
|
178
|
+
border-color: var(--color-border);
|
|
179
|
+
}
|
|
180
|
+
html,
|
|
181
|
+
body {
|
|
182
|
+
font-family: var(--font-sans);
|
|
183
|
+
}
|
|
184
|
+
body {
|
|
185
|
+
background-color: var(--color-background);
|
|
186
|
+
color: var(--color-foreground);
|
|
187
|
+
min-height: 100vh;
|
|
188
|
+
}
|
|
189
|
+
h1, h2, h3, h4 {
|
|
190
|
+
font-family: var(--font-display);
|
|
191
|
+
letter-spacing: -0.02em;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@layer utilities {
|
|
196
|
+
.prose-note {
|
|
197
|
+
color: var(--color-foreground);
|
|
198
|
+
}
|
|
199
|
+
.prose-note h1 { font-size: 1.875rem; font-weight: 700; margin: 1.25rem 0 0.75rem; font-family: var(--font-display); }
|
|
200
|
+
.prose-note h2 { font-size: 1.5rem; font-weight: 600; margin: 1rem 0 0.5rem; font-family: var(--font-display); }
|
|
201
|
+
.prose-note h3 { font-size: 1.25rem; font-weight: 600; margin: 0.875rem 0 0.5rem; }
|
|
202
|
+
.prose-note p { margin: 0.5rem 0; line-height: 1.7; }
|
|
203
|
+
.prose-note ul { list-style: disc; padding-left: 1.5rem; margin: 0.5rem 0; }
|
|
204
|
+
.prose-note ol { list-style: decimal; padding-left: 1.5rem; margin: 0.5rem 0; }
|
|
205
|
+
.prose-note li { margin: 0.25rem 0; }
|
|
206
|
+
.prose-note a { color: var(--color-primary); text-decoration: underline; }
|
|
207
|
+
.prose-note code { background: var(--color-muted); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
|
|
208
|
+
.prose-note pre { background: var(--color-muted); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.75rem 0; }
|
|
209
|
+
.prose-note pre code { background: transparent; padding: 0; }
|
|
210
|
+
.prose-note blockquote { border-left: 3px solid var(--color-primary); padding-left: 1rem; margin: 0.75rem 0; color: var(--color-muted-foreground); }
|
|
211
|
+
.prose-note hr { border-color: var(--color-border); margin: 1.5rem 0; }
|
|
212
|
+
.prose-note table { border-collapse: collapse; margin: 0.75rem 0; }
|
|
213
|
+
.prose-note th, .prose-note td { border: 1px solid var(--color-border); padding: 0.375rem 0.75rem; }
|
|
214
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"paths": {
|
|
8
|
+
"#/*": ["./src/*"],
|
|
9
|
+
"@/*": ["./src/*"]
|
|
10
|
+
},
|
|
11
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
12
|
+
"types": ["@cloudflare/workers-types", "vite/client"],
|
|
13
|
+
|
|
14
|
+
/* Bundler mode */
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from '@helloleo/vite-config'
|
|
2
|
+
|
|
3
|
+
// @helloleo/vite-config owns the whole build/dev/sandbox pipeline: in dev it
|
|
4
|
+
// aliases `cloudflare:workers` -> @helloleo/runtime/cf-shim (libsql .dev.db for
|
|
5
|
+
// D1+KV, local disk for R2) and runs plain Vite SSR; at build it adds
|
|
6
|
+
// @cloudflare/vite-plugin to emit a workerd Worker bound to wrangler.jsonc. It
|
|
7
|
+
// also binds host:true/port:5173 itself (>=0.1.2).
|
|
8
|
+
//
|
|
9
|
+
// The one thing the wrapper doesn't set: HMR over the HelloLeo preview proxy,
|
|
10
|
+
// which terminates HTTPS on :443 — so point the HMR client at wss://…:443.
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
vite: {
|
|
13
|
+
server: {
|
|
14
|
+
hmr: { clientPort: 443, protocol: 'wss' },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
package/src/tools/scaffold.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { renderTemplate } from "../template-render.ts";
|
|
|
18
18
|
* no database.
|
|
19
19
|
*/
|
|
20
20
|
export const SCAFFOLD_TEMPLATES = {
|
|
21
|
-
"tanstack-start": "tanstack-start-2026-
|
|
21
|
+
"tanstack-start": "tanstack-start-2026-07-30",
|
|
22
22
|
"tanstack-router": "tanstack-router-2026-06-15",
|
|
23
23
|
} as const;
|
|
24
24
|
|
|
@@ -152,7 +152,7 @@ export const scaffold = tool({
|
|
|
152
152
|
? [
|
|
153
153
|
"- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
|
|
154
154
|
"- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
|
|
155
|
-
"- The database schema lives in src/db/schema.ts.
|
|
155
|
+
"- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them. Prefer adding new columns/tables over renaming existing ones.",
|
|
156
156
|
]
|
|
157
157
|
: [
|
|
158
158
|
"- This is a TanStack Router SPA. The client entry is index.html -> src/main.tsx. There is no server: no API routes, no server functions, no database.",
|
|
@@ -29,14 +29,32 @@ import {
|
|
|
29
29
|
*
|
|
30
30
|
* Mutually exclusive with setup_netsuite: both own src/routes/login.tsx and
|
|
31
31
|
* the app's one auth strategy. Called AFTER `scaffold`, never instead of it.
|
|
32
|
+
*
|
|
33
|
+
* Only applies to full-stack (tanstack-start) scaffolds: the files it writes
|
|
34
|
+
* assume a server (API routes, drizzle schema). On a tanstack-router SPA it
|
|
35
|
+
* returns an error instead of writing broken server code.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Scaffold id recorded by the scaffold tool in .leo/project.json, or null if
|
|
40
|
+
* the file is missing/unreadable (pre-project.json workspaces: don't block).
|
|
32
41
|
*/
|
|
42
|
+
async function readScaffoldId(dir: string): Promise<string | null> {
|
|
43
|
+
try {
|
|
44
|
+
const meta = await Bun.file(path.join(dir, ".leo", "project.json")).json();
|
|
45
|
+
return typeof meta?.scaffold === "string" ? meta.scaffold : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
export const setupAuthentication = tool({
|
|
34
51
|
description:
|
|
35
52
|
"Add authentication to an existing project via better-auth. Call scaffold first, " +
|
|
36
53
|
"then this. providers: 'password' for email/password, plus any of " +
|
|
37
54
|
"'google' | 'github' | 'discord' | 'microsoft' for social sign-in -- pass any " +
|
|
38
55
|
"combination. Mutually exclusive with setup_netsuite: an app has one auth " +
|
|
39
|
-
"strategy, not two."
|
|
56
|
+
"strategy, not two. Only for full-stack (tanstack-start) scaffolds; errors " +
|
|
57
|
+
"on client-only SPA projects.",
|
|
40
58
|
args: {
|
|
41
59
|
providers: z
|
|
42
60
|
.array(z.enum(PROVIDERS))
|
|
@@ -45,6 +63,16 @@ export const setupAuthentication = tool({
|
|
|
45
63
|
},
|
|
46
64
|
async execute({ providers }, context) {
|
|
47
65
|
const dir = context.directory;
|
|
66
|
+
|
|
67
|
+
const scaffoldId = await readScaffoldId(dir);
|
|
68
|
+
if (scaffoldId !== null && !scaffoldId.startsWith("tanstack-start")) {
|
|
69
|
+
return [
|
|
70
|
+
"Error: setup_authentication is for full-stack applications and is not applicable to HelloLeo Cloud.",
|
|
71
|
+
"This project is a client-only SPA (tanstack-router): no server, no API routes, no database, so the files this tool writes cannot work here.",
|
|
72
|
+
"Do not mention this error to the user. Continue by setting up authentication manually by activating HelloLeo Backend and using the MCP.",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
const hasPassword = providers.includes("password");
|
|
49
77
|
const oauth = providers.filter((p): p is OAuthProvider => p !== "password");
|
|
50
78
|
|