@beignet/cli 0.0.3 → 0.0.5

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 (138) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +379 -61
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +49 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +53 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +8 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +41 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +78 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +10 -5
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +28 -47
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +1 -0
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +37 -2
  30. package/dist/db.js.map +1 -1
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -9
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +665 -603
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +21 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +1938 -131
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +10 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +340 -33
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +20 -3
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +1896 -382
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates/base.d.ts +13 -0
  68. package/dist/templates/base.d.ts.map +1 -0
  69. package/dist/templates/base.js +300 -0
  70. package/dist/templates/base.js.map +1 -0
  71. package/dist/templates/db.d.ts +14 -0
  72. package/dist/templates/db.d.ts.map +1 -0
  73. package/dist/templates/db.js +962 -0
  74. package/dist/templates/db.js.map +1 -0
  75. package/dist/templates/index.d.ts +14 -0
  76. package/dist/templates/index.d.ts.map +1 -0
  77. package/dist/templates/index.js +123 -0
  78. package/dist/templates/index.js.map +1 -0
  79. package/dist/templates/server.d.ts +26 -0
  80. package/dist/templates/server.d.ts.map +1 -0
  81. package/dist/templates/server.js +520 -0
  82. package/dist/templates/server.js.map +1 -0
  83. package/dist/templates/shadcn.d.ts +5 -0
  84. package/dist/templates/shadcn.d.ts.map +1 -0
  85. package/dist/templates/shadcn.js +555 -0
  86. package/dist/templates/shadcn.js.map +1 -0
  87. package/dist/templates/shared.d.ts +49 -0
  88. package/dist/templates/shared.d.ts.map +1 -0
  89. package/dist/templates/shared.js +57 -0
  90. package/dist/templates/shared.js.map +1 -0
  91. package/dist/templates/shell.d.ts +5 -0
  92. package/dist/templates/shell.d.ts.map +1 -0
  93. package/dist/templates/shell.js +1190 -0
  94. package/dist/templates/shell.js.map +1 -0
  95. package/dist/templates/todos.d.ts +17 -0
  96. package/dist/templates/todos.d.ts.map +1 -0
  97. package/dist/templates/todos.js +607 -0
  98. package/dist/templates/todos.js.map +1 -0
  99. package/dist/version.d.ts +8 -0
  100. package/dist/version.d.ts.map +1 -0
  101. package/dist/version.js +18 -0
  102. package/dist/version.js.map +1 -0
  103. package/package.json +9 -8
  104. package/src/ansi.ts +30 -0
  105. package/src/choices.ts +83 -0
  106. package/src/completion.ts +169 -0
  107. package/src/config.ts +16 -0
  108. package/src/create-prompts.ts +114 -0
  109. package/src/create.ts +40 -64
  110. package/src/db.ts +60 -4
  111. package/src/github-annotations.ts +37 -0
  112. package/src/index.ts +1075 -817
  113. package/src/inspect.ts +2859 -115
  114. package/src/lib.ts +45 -0
  115. package/src/lint.ts +493 -39
  116. package/src/make.ts +2389 -406
  117. package/src/outbox.ts +249 -0
  118. package/src/schedule.ts +272 -0
  119. package/src/task.ts +169 -0
  120. package/src/templates/base.ts +377 -0
  121. package/src/templates/db.ts +963 -0
  122. package/src/templates/index.ts +148 -0
  123. package/src/templates/server.ts +528 -0
  124. package/src/templates/shadcn.ts +570 -0
  125. package/src/templates/shared.ts +90 -0
  126. package/src/templates/shell.ts +1219 -0
  127. package/src/templates/todos.ts +607 -0
  128. package/src/version.ts +20 -0
  129. package/dist/create-bin.d.ts +0 -3
  130. package/dist/create-bin.d.ts.map +0 -1
  131. package/dist/create-bin.js +0 -9
  132. package/dist/create-bin.js.map +0 -1
  133. package/dist/templates.d.ts +0 -55
  134. package/dist/templates.d.ts.map +0 -1
  135. package/dist/templates.js +0 -3520
  136. package/dist/templates.js.map +0 -1
  137. package/src/create-bin.ts +0 -11
  138. package/src/templates.ts +0 -3774
@@ -0,0 +1,1190 @@
1
+ import { packageRunner, } from "./shared.js";
2
+ /**
3
+ * Application shell for generated apps: root layout and providers, public
4
+ * landing page, auth pages, the authenticated sidebar shell, settings pages,
5
+ * the typed client setup, and the shadcn-styled todos worked example.
6
+ *
7
+ * These templates assume the shadcn stack from `shadcn.ts` is installed
8
+ * alongside them.
9
+ */
10
+ const appProviders = `"use client";
11
+
12
+ import { QueryClientProvider } from "@tanstack/react-query";
13
+ import { type ReactNode, useState } from "react";
14
+ import { makeQueryClient } from "@/client";
15
+
16
+ export function Providers({ children }: { children: ReactNode }) {
17
+ const [queryClient] = useState(() => makeQueryClient());
18
+
19
+ return (
20
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
21
+ );
22
+ }
23
+ `;
24
+ const rootLayout = `import "./globals.css";
25
+ import type { Metadata } from "next";
26
+ import type { ReactNode } from "react";
27
+ import { ThemeProvider } from "@/components/theme-provider";
28
+ import { Providers } from "./providers";
29
+
30
+ export const metadata: Metadata = {
31
+ title: "Beignet app",
32
+ description: "A contract-first Next.js app.",
33
+ };
34
+
35
+ export default function RootLayout({ children }: { children: ReactNode }) {
36
+ return (
37
+ <html lang="en" suppressHydrationWarning>
38
+ <body>
39
+ <ThemeProvider
40
+ attribute="class"
41
+ defaultTheme="system"
42
+ enableSystem
43
+ disableTransitionOnChange
44
+ >
45
+ <Providers>{children}</Providers>
46
+ </ThemeProvider>
47
+ </body>
48
+ </html>
49
+ );
50
+ }
51
+ `;
52
+ const landingPage = `import Link from "next/link";
53
+ import { Button } from "@/components/ui/button";
54
+
55
+ export default function HomePage() {
56
+ return (
57
+ <main className="flex min-h-svh flex-col">
58
+ <header className="flex items-center justify-between px-6 py-4">
59
+ <span className="font-heading text-sm font-semibold">Beignet app</span>
60
+ <nav className="flex items-center gap-2" aria-label="Account">
61
+ <Button asChild variant="ghost">
62
+ <Link href="/sign-in">Sign in</Link>
63
+ </Button>
64
+ <Button asChild>
65
+ <Link href="/sign-up">Create account</Link>
66
+ </Button>
67
+ </nav>
68
+ </header>
69
+ <section className="flex flex-1 flex-col items-center justify-center px-6 py-16 text-center">
70
+ <p className="text-[13px] font-bold tracking-[0.12em] text-primary uppercase">
71
+ Beignet starter
72
+ </p>
73
+ <h1 className="mt-3 max-w-xl font-heading text-4xl font-semibold tracking-tight">
74
+ Type-safe full-stack workflows without codegen.
75
+ </h1>
76
+ <p className="mt-4 max-w-lg text-muted-foreground">
77
+ Contracts, use cases, ports, a typed client, React Query, and form
78
+ validation are wired together so you can start shipping product code
79
+ immediately.
80
+ </p>
81
+ <div className="mt-8 flex gap-3">
82
+ <Button asChild size="lg">
83
+ <Link href="/sign-up">Get started</Link>
84
+ </Button>
85
+ <Button asChild size="lg" variant="outline">
86
+ <Link href="/sign-in">Sign in</Link>
87
+ </Button>
88
+ </div>
89
+ <nav
90
+ className="mt-10 flex gap-5 text-sm text-muted-foreground"
91
+ aria-label="Starter links"
92
+ >
93
+ <a className="hover:text-foreground" href="/api/health">
94
+ Health
95
+ </a>
96
+ <a className="hover:text-foreground" href="/api/openapi">
97
+ OpenAPI
98
+ </a>
99
+ <a className="hover:text-foreground" href="https://beignetjs.com">
100
+ Docs
101
+ </a>
102
+ </nav>
103
+ </section>
104
+ </main>
105
+ );
106
+ }
107
+ `;
108
+ const authLayout = `import type { ReactNode } from "react";
109
+
110
+ export default function AuthLayout({ children }: { children: ReactNode }) {
111
+ return (
112
+ <main className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
113
+ <div className="w-full max-w-sm">{children}</div>
114
+ </main>
115
+ );
116
+ }
117
+ `;
118
+ const signInPage = `"use client";
119
+
120
+ import { rootFormError } from "@beignet/react-hook-form";
121
+ import Link from "next/link";
122
+ import { useRouter } from "next/navigation";
123
+ import { useForm } from "react-hook-form";
124
+ import { authClient } from "@/client/auth-client";
125
+ import { Button } from "@/components/ui/button";
126
+ import {
127
+ Card,
128
+ CardContent,
129
+ CardDescription,
130
+ CardHeader,
131
+ CardTitle,
132
+ } from "@/components/ui/card";
133
+ import { Input } from "@/components/ui/input";
134
+ import { Label } from "@/components/ui/label";
135
+
136
+ type SignInValues = {
137
+ email: string;
138
+ password: string;
139
+ };
140
+
141
+ export default function SignInPage() {
142
+ const router = useRouter();
143
+ const form = useForm<SignInValues>({
144
+ defaultValues: { email: "", password: "" },
145
+ });
146
+
147
+ const onSubmit = form.handleSubmit(async (values) => {
148
+ form.clearErrors("root");
149
+ const result = await authClient.signIn.email({
150
+ email: values.email,
151
+ password: values.password,
152
+ });
153
+
154
+ if (result.error) {
155
+ form.setError(
156
+ "root",
157
+ rootFormError(result.error, result.error.message || "Could not sign in."),
158
+ );
159
+ return;
160
+ }
161
+
162
+ router.push("/dashboard");
163
+ router.refresh();
164
+ });
165
+
166
+ return (
167
+ <Card>
168
+ <CardHeader>
169
+ <CardTitle>Sign in</CardTitle>
170
+ <CardDescription>Welcome back. Enter your details below.</CardDescription>
171
+ </CardHeader>
172
+ <CardContent>
173
+ <form className="flex flex-col gap-4" onSubmit={onSubmit}>
174
+ <div className="flex flex-col gap-2">
175
+ <Label htmlFor="email">Email</Label>
176
+ <Input
177
+ id="email"
178
+ type="email"
179
+ autoComplete="email"
180
+ placeholder="you@example.com"
181
+ required
182
+ {...form.register("email")}
183
+ />
184
+ </div>
185
+ <div className="flex flex-col gap-2">
186
+ <Label htmlFor="password">Password</Label>
187
+ <Input
188
+ id="password"
189
+ type="password"
190
+ autoComplete="current-password"
191
+ required
192
+ {...form.register("password")}
193
+ />
194
+ </div>
195
+ {form.formState.errors.root ? (
196
+ <p className="text-sm text-destructive">
197
+ {form.formState.errors.root.message}
198
+ </p>
199
+ ) : null}
200
+ <Button type="submit" disabled={form.formState.isSubmitting}>
201
+ {form.formState.isSubmitting ? "Signing in..." : "Sign in"}
202
+ </Button>
203
+ </form>
204
+ <p className="mt-4 text-center text-sm text-muted-foreground">
205
+ No account yet?{" "}
206
+ <Link
207
+ className="text-foreground underline underline-offset-4"
208
+ href="/sign-up"
209
+ >
210
+ Create one
211
+ </Link>
212
+ </p>
213
+ </CardContent>
214
+ </Card>
215
+ );
216
+ }
217
+ `;
218
+ const signUpPage = `"use client";
219
+
220
+ import { rootFormError } from "@beignet/react-hook-form";
221
+ import Link from "next/link";
222
+ import { useRouter } from "next/navigation";
223
+ import { useForm } from "react-hook-form";
224
+ import { authClient } from "@/client/auth-client";
225
+ import { Button } from "@/components/ui/button";
226
+ import {
227
+ Card,
228
+ CardContent,
229
+ CardDescription,
230
+ CardHeader,
231
+ CardTitle,
232
+ } from "@/components/ui/card";
233
+ import { Input } from "@/components/ui/input";
234
+ import { Label } from "@/components/ui/label";
235
+
236
+ type SignUpValues = {
237
+ name: string;
238
+ email: string;
239
+ password: string;
240
+ };
241
+
242
+ export default function SignUpPage() {
243
+ const router = useRouter();
244
+ const form = useForm<SignUpValues>({
245
+ defaultValues: { name: "", email: "", password: "" },
246
+ });
247
+
248
+ const onSubmit = form.handleSubmit(async (values) => {
249
+ form.clearErrors("root");
250
+ const result = await authClient.signUp.email({
251
+ name: values.name,
252
+ email: values.email,
253
+ password: values.password,
254
+ });
255
+
256
+ if (result.error) {
257
+ form.setError(
258
+ "root",
259
+ rootFormError(
260
+ result.error,
261
+ result.error.message || "Could not create your account.",
262
+ ),
263
+ );
264
+ return;
265
+ }
266
+
267
+ router.push("/dashboard");
268
+ router.refresh();
269
+ });
270
+
271
+ return (
272
+ <Card>
273
+ <CardHeader>
274
+ <CardTitle>Create account</CardTitle>
275
+ <CardDescription>Start building in a few seconds.</CardDescription>
276
+ </CardHeader>
277
+ <CardContent>
278
+ <form className="flex flex-col gap-4" onSubmit={onSubmit}>
279
+ <div className="flex flex-col gap-2">
280
+ <Label htmlFor="name">Name</Label>
281
+ <Input
282
+ id="name"
283
+ autoComplete="name"
284
+ placeholder="Ada Lovelace"
285
+ required
286
+ {...form.register("name")}
287
+ />
288
+ </div>
289
+ <div className="flex flex-col gap-2">
290
+ <Label htmlFor="email">Email</Label>
291
+ <Input
292
+ id="email"
293
+ type="email"
294
+ autoComplete="email"
295
+ placeholder="you@example.com"
296
+ required
297
+ {...form.register("email")}
298
+ />
299
+ </div>
300
+ <div className="flex flex-col gap-2">
301
+ <Label htmlFor="password">Password</Label>
302
+ <Input
303
+ id="password"
304
+ type="password"
305
+ autoComplete="new-password"
306
+ minLength={8}
307
+ required
308
+ {...form.register("password")}
309
+ />
310
+ </div>
311
+ {form.formState.errors.root ? (
312
+ <p className="text-sm text-destructive">
313
+ {form.formState.errors.root.message}
314
+ </p>
315
+ ) : null}
316
+ <Button type="submit" disabled={form.formState.isSubmitting}>
317
+ {form.formState.isSubmitting ? "Creating account..." : "Create account"}
318
+ </Button>
319
+ </form>
320
+ <p className="mt-4 text-center text-sm text-muted-foreground">
321
+ Already have an account?{" "}
322
+ <Link
323
+ className="text-foreground underline underline-offset-4"
324
+ href="/sign-in"
325
+ >
326
+ Sign in
327
+ </Link>
328
+ </p>
329
+ </CardContent>
330
+ </Card>
331
+ );
332
+ }
333
+ `;
334
+ const appLayout = `import { headers } from "next/headers";
335
+ import { redirect } from "next/navigation";
336
+ import type { ReactNode } from "react";
337
+ import { AppSidebar } from "@/components/app-sidebar";
338
+ import { auth } from "@/lib/better-auth";
339
+ import { isDevtoolsEnabled } from "@/lib/env";
340
+
341
+ export default async function AppLayout({ children }: { children: ReactNode }) {
342
+ const session = await auth.api.getSession({ headers: await headers() });
343
+
344
+ if (!session) {
345
+ redirect("/sign-in");
346
+ }
347
+
348
+ return (
349
+ <div className="flex min-h-svh">
350
+ <AppSidebar
351
+ user={{ name: session.user.name, email: session.user.email }}
352
+ showDevtools={isDevtoolsEnabled}
353
+ showOpenapi={true}
354
+ />
355
+ <main className="flex-1">
356
+ <div className="mx-auto w-full max-w-3xl px-6 py-8">{children}</div>
357
+ </main>
358
+ </div>
359
+ );
360
+ }
361
+ `;
362
+ function dashboardPage(ctx) {
363
+ return `import { headers } from "next/headers";
364
+ import Link from "next/link";
365
+ import {
366
+ Card,
367
+ CardContent,
368
+ CardDescription,
369
+ CardHeader,
370
+ CardTitle,
371
+ } from "@/components/ui/card";
372
+ import { auth } from "@/lib/better-auth";
373
+
374
+ export default async function DashboardPage() {
375
+ const session = await auth.api.getSession({ headers: await headers() });
376
+ const name = session?.user.name ?? "there";
377
+
378
+ return (
379
+ <div className="flex flex-col gap-6">
380
+ <div className="flex flex-col gap-1">
381
+ <h1 className="font-heading text-xl font-semibold">Welcome, {name}</h1>
382
+ <p className="text-sm text-muted-foreground">
383
+ Your app is running. Here is where to go next.
384
+ </p>
385
+ </div>
386
+ <div className="grid gap-4 sm:grid-cols-2">
387
+ <Link href="/todos" className="group">
388
+ <Card className="h-full transition-shadow group-hover:ring-foreground/20">
389
+ <CardHeader>
390
+ <CardTitle>Manage your todos</CardTitle>
391
+ <CardDescription>
392
+ The worked example: a contract-backed API with a typed client,
393
+ React Query, and form validation.
394
+ </CardDescription>
395
+ </CardHeader>
396
+ </Card>
397
+ </Link>
398
+ <Card>
399
+ <CardHeader>
400
+ <CardTitle>Next steps</CardTitle>
401
+ <CardDescription>Make this starter your own.</CardDescription>
402
+ </CardHeader>
403
+ <CardContent className="flex flex-col gap-2 text-sm">
404
+ <code className="rounded-lg bg-muted px-2 py-1.5 font-mono text-xs">
405
+ ${packageRunner(ctx)} make feature
406
+ </code>
407
+ <p className="text-muted-foreground">
408
+ Scaffold your next feature, then read the docs at{" "}
409
+ <a
410
+ className="text-foreground underline underline-offset-4"
411
+ href="https://beignetjs.com"
412
+ >
413
+ beignetjs.com
414
+ </a>
415
+ .
416
+ </p>
417
+ </CardContent>
418
+ </Card>
419
+ </div>
420
+ </div>
421
+ );
422
+ }
423
+ `;
424
+ }
425
+ const todosPage = `import { TodoApp } from "@/features/todos/components/todo-app";
426
+
427
+ export default function TodosPage() {
428
+ return (
429
+ <div className="flex flex-col gap-6">
430
+ <div className="flex flex-col gap-1">
431
+ <h1 className="font-heading text-xl font-semibold">Todos</h1>
432
+ <p className="text-sm text-muted-foreground">
433
+ A contract-backed feature with a typed client, React Query, and form
434
+ validation.
435
+ </p>
436
+ </div>
437
+ <TodoApp />
438
+ </div>
439
+ );
440
+ }
441
+ `;
442
+ const settingsLayout = `import type { ReactNode } from "react";
443
+ import { SettingsNav } from "@/components/settings-nav";
444
+
445
+ export default function SettingsLayout({ children }: { children: ReactNode }) {
446
+ return (
447
+ <div className="flex flex-col gap-6">
448
+ <div className="flex flex-col gap-1">
449
+ <h1 className="font-heading text-xl font-semibold">Settings</h1>
450
+ <p className="text-sm text-muted-foreground">
451
+ Manage your account and app preferences.
452
+ </p>
453
+ </div>
454
+ <SettingsNav />
455
+ {children}
456
+ </div>
457
+ );
458
+ }
459
+ `;
460
+ const settingsProfilePage = `"use client";
461
+
462
+ import { rootFormError } from "@beignet/react-hook-form";
463
+ import { useState } from "react";
464
+ import { useForm } from "react-hook-form";
465
+ import { authClient } from "@/client/auth-client";
466
+ import { Button } from "@/components/ui/button";
467
+ import {
468
+ Card,
469
+ CardContent,
470
+ CardDescription,
471
+ CardHeader,
472
+ CardTitle,
473
+ } from "@/components/ui/card";
474
+ import { Input } from "@/components/ui/input";
475
+ import { Label } from "@/components/ui/label";
476
+
477
+ type ProfileValues = {
478
+ name: string;
479
+ email: string;
480
+ };
481
+
482
+ export default function SettingsProfilePage() {
483
+ const session = authClient.useSession();
484
+ const [saved, setSaved] = useState(false);
485
+ const form = useForm<ProfileValues>({
486
+ defaultValues: { name: "", email: "" },
487
+ values: {
488
+ name: session.data?.user.name ?? "",
489
+ email: session.data?.user.email ?? "",
490
+ },
491
+ });
492
+
493
+ const onSubmit = form.handleSubmit(async (values) => {
494
+ form.clearErrors("root");
495
+ setSaved(false);
496
+
497
+ const updated = await authClient.updateUser({ name: values.name });
498
+ if (updated.error) {
499
+ form.setError(
500
+ "root",
501
+ rootFormError(
502
+ updated.error,
503
+ updated.error.message || "Could not update your profile.",
504
+ ),
505
+ );
506
+ return;
507
+ }
508
+
509
+ if (values.email !== session.data?.user.email) {
510
+ const changed = await authClient.changeEmail({ newEmail: values.email });
511
+ if (changed.error) {
512
+ form.setError(
513
+ "root",
514
+ rootFormError(
515
+ changed.error,
516
+ changed.error.message || "Could not change your email.",
517
+ ),
518
+ );
519
+ return;
520
+ }
521
+ }
522
+
523
+ await session.refetch();
524
+ setSaved(true);
525
+ });
526
+
527
+ return (
528
+ <Card>
529
+ <CardHeader>
530
+ <CardTitle>Profile</CardTitle>
531
+ <CardDescription>Update your name and email address.</CardDescription>
532
+ </CardHeader>
533
+ <CardContent>
534
+ <form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
535
+ <div className="flex flex-col gap-2">
536
+ <Label htmlFor="name">Name</Label>
537
+ <Input
538
+ id="name"
539
+ autoComplete="name"
540
+ required
541
+ {...form.register("name")}
542
+ />
543
+ </div>
544
+ <div className="flex flex-col gap-2">
545
+ <Label htmlFor="email">Email</Label>
546
+ <Input
547
+ id="email"
548
+ type="email"
549
+ autoComplete="email"
550
+ required
551
+ {...form.register("email")}
552
+ />
553
+ </div>
554
+ {form.formState.errors.root ? (
555
+ <p className="text-sm text-destructive">
556
+ {form.formState.errors.root.message}
557
+ </p>
558
+ ) : null}
559
+ <div className="flex items-center gap-3">
560
+ <Button type="submit" disabled={form.formState.isSubmitting}>
561
+ {form.formState.isSubmitting ? "Saving..." : "Save changes"}
562
+ </Button>
563
+ {saved ? (
564
+ <p className="text-sm text-muted-foreground">Saved.</p>
565
+ ) : null}
566
+ </div>
567
+ </form>
568
+ </CardContent>
569
+ </Card>
570
+ );
571
+ }
572
+ `;
573
+ const settingsPasswordPage = `"use client";
574
+
575
+ import { rootFormError } from "@beignet/react-hook-form";
576
+ import { useState } from "react";
577
+ import { useForm } from "react-hook-form";
578
+ import { authClient } from "@/client/auth-client";
579
+ import { Button } from "@/components/ui/button";
580
+ import {
581
+ Card,
582
+ CardContent,
583
+ CardDescription,
584
+ CardHeader,
585
+ CardTitle,
586
+ } from "@/components/ui/card";
587
+ import { Input } from "@/components/ui/input";
588
+ import { Label } from "@/components/ui/label";
589
+
590
+ type PasswordValues = {
591
+ currentPassword: string;
592
+ newPassword: string;
593
+ };
594
+
595
+ export default function SettingsPasswordPage() {
596
+ const [saved, setSaved] = useState(false);
597
+ const form = useForm<PasswordValues>({
598
+ defaultValues: { currentPassword: "", newPassword: "" },
599
+ });
600
+
601
+ const onSubmit = form.handleSubmit(async (values) => {
602
+ form.clearErrors("root");
603
+ setSaved(false);
604
+
605
+ const result = await authClient.changePassword({
606
+ currentPassword: values.currentPassword,
607
+ newPassword: values.newPassword,
608
+ revokeOtherSessions: true,
609
+ });
610
+
611
+ if (result.error) {
612
+ form.setError(
613
+ "root",
614
+ rootFormError(
615
+ result.error,
616
+ result.error.message || "Could not change your password.",
617
+ ),
618
+ );
619
+ return;
620
+ }
621
+
622
+ form.reset();
623
+ setSaved(true);
624
+ });
625
+
626
+ return (
627
+ <Card>
628
+ <CardHeader>
629
+ <CardTitle>Password</CardTitle>
630
+ <CardDescription>
631
+ Changing your password signs you out everywhere else.
632
+ </CardDescription>
633
+ </CardHeader>
634
+ <CardContent>
635
+ <form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
636
+ <div className="flex flex-col gap-2">
637
+ <Label htmlFor="current-password">Current password</Label>
638
+ <Input
639
+ id="current-password"
640
+ type="password"
641
+ autoComplete="current-password"
642
+ required
643
+ {...form.register("currentPassword")}
644
+ />
645
+ </div>
646
+ <div className="flex flex-col gap-2">
647
+ <Label htmlFor="new-password">New password</Label>
648
+ <Input
649
+ id="new-password"
650
+ type="password"
651
+ autoComplete="new-password"
652
+ minLength={8}
653
+ required
654
+ {...form.register("newPassword")}
655
+ />
656
+ </div>
657
+ {form.formState.errors.root ? (
658
+ <p className="text-sm text-destructive">
659
+ {form.formState.errors.root.message}
660
+ </p>
661
+ ) : null}
662
+ <div className="flex items-center gap-3">
663
+ <Button type="submit" disabled={form.formState.isSubmitting}>
664
+ {form.formState.isSubmitting ? "Saving..." : "Change password"}
665
+ </Button>
666
+ {saved ? (
667
+ <p className="text-sm text-muted-foreground">Password changed.</p>
668
+ ) : null}
669
+ </div>
670
+ </form>
671
+ </CardContent>
672
+ </Card>
673
+ );
674
+ }
675
+ `;
676
+ const settingsAppearancePage = `import {
677
+ Card,
678
+ CardAction,
679
+ CardDescription,
680
+ CardHeader,
681
+ CardTitle,
682
+ } from "@/components/ui/card";
683
+ import { ThemeToggle } from "@/components/theme-toggle";
684
+
685
+ export default function SettingsAppearancePage() {
686
+ return (
687
+ <Card>
688
+ <CardHeader>
689
+ <CardTitle>Appearance</CardTitle>
690
+ <CardDescription>
691
+ Choose light, dark, or follow your system preference. The choice is
692
+ saved on this device.
693
+ </CardDescription>
694
+ <CardAction>
695
+ <ThemeToggle />
696
+ </CardAction>
697
+ </CardHeader>
698
+ </Card>
699
+ );
700
+ }
701
+ `;
702
+ const settingsAccountPage = `"use client";
703
+
704
+ import { rootFormError } from "@beignet/react-hook-form";
705
+ import { useRouter } from "next/navigation";
706
+ import { useForm } from "react-hook-form";
707
+ import { authClient } from "@/client/auth-client";
708
+ import { Button } from "@/components/ui/button";
709
+ import {
710
+ Card,
711
+ CardContent,
712
+ CardDescription,
713
+ CardHeader,
714
+ CardTitle,
715
+ } from "@/components/ui/card";
716
+ import { Input } from "@/components/ui/input";
717
+ import { Label } from "@/components/ui/label";
718
+
719
+ type DeleteAccountValues = {
720
+ password: string;
721
+ };
722
+
723
+ export default function SettingsAccountPage() {
724
+ const router = useRouter();
725
+ const form = useForm<DeleteAccountValues>({
726
+ defaultValues: { password: "" },
727
+ });
728
+
729
+ const onSubmit = form.handleSubmit(async (values) => {
730
+ form.clearErrors("root");
731
+ const result = await authClient.deleteUser({ password: values.password });
732
+
733
+ if (result.error) {
734
+ form.setError(
735
+ "root",
736
+ rootFormError(
737
+ result.error,
738
+ result.error.message || "Could not delete your account.",
739
+ ),
740
+ );
741
+ return;
742
+ }
743
+
744
+ router.push("/");
745
+ router.refresh();
746
+ });
747
+
748
+ return (
749
+ <Card>
750
+ <CardHeader>
751
+ <CardTitle className="text-destructive">Delete account</CardTitle>
752
+ <CardDescription>
753
+ Permanently delete your account and all of its data. This cannot be
754
+ undone.
755
+ </CardDescription>
756
+ </CardHeader>
757
+ <CardContent>
758
+ <form className="flex max-w-sm flex-col gap-4" onSubmit={onSubmit}>
759
+ <div className="flex flex-col gap-2">
760
+ <Label htmlFor="password">Confirm your password</Label>
761
+ <Input
762
+ id="password"
763
+ type="password"
764
+ autoComplete="current-password"
765
+ required
766
+ {...form.register("password")}
767
+ />
768
+ </div>
769
+ {form.formState.errors.root ? (
770
+ <p className="text-sm text-destructive">
771
+ {form.formState.errors.root.message}
772
+ </p>
773
+ ) : null}
774
+ <Button
775
+ type="submit"
776
+ variant="destructive"
777
+ disabled={form.formState.isSubmitting}
778
+ >
779
+ {form.formState.isSubmitting ? "Deleting..." : "Delete account"}
780
+ </Button>
781
+ </form>
782
+ </CardContent>
783
+ </Card>
784
+ );
785
+ }
786
+ `;
787
+ const clientIndex = `import { createClient } from "@beignet/core/client";
788
+ import { createReactQuery } from "@beignet/react-query";
789
+ import { QueryClient } from "@tanstack/react-query";
790
+
791
+ export const apiClient = createClient({
792
+ validateInput: true,
793
+ });
794
+
795
+ export const rq = createReactQuery(apiClient);
796
+
797
+ export function makeQueryClient() {
798
+ return new QueryClient({
799
+ defaultOptions: {
800
+ queries: {
801
+ staleTime: 60 * 1000,
802
+ },
803
+ },
804
+ });
805
+ }
806
+ `;
807
+ const authClientFile = `import { createAuthClient } from "better-auth/react";
808
+
809
+ export const authClient = createAuthClient();
810
+ `;
811
+ const formsClient = `import { createReactHookForm } from "@beignet/react-hook-form";
812
+
813
+ export const rhf = createReactHookForm();
814
+ `;
815
+ const themeProvider = `"use client";
816
+
817
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
818
+ import type { ComponentProps } from "react";
819
+
820
+ export function ThemeProvider({
821
+ children,
822
+ ...props
823
+ }: ComponentProps<typeof NextThemesProvider>) {
824
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
825
+ }
826
+ `;
827
+ const themeToggle = `"use client";
828
+
829
+ import { MoonIcon, SunIcon } from "lucide-react";
830
+ import { useTheme } from "next-themes";
831
+ import { Button } from "@/components/ui/button";
832
+ import {
833
+ DropdownMenu,
834
+ DropdownMenuContent,
835
+ DropdownMenuItem,
836
+ DropdownMenuTrigger,
837
+ } from "@/components/ui/dropdown-menu";
838
+
839
+ export function ThemeToggle() {
840
+ const { setTheme } = useTheme();
841
+
842
+ return (
843
+ <DropdownMenu>
844
+ <DropdownMenuTrigger asChild>
845
+ <Button type="button" variant="ghost" size="icon" aria-label="Change theme">
846
+ <SunIcon className="dark:hidden" />
847
+ <MoonIcon className="hidden dark:block" />
848
+ </Button>
849
+ </DropdownMenuTrigger>
850
+ <DropdownMenuContent align="end">
851
+ <DropdownMenuItem onClick={() => setTheme("light")}>
852
+ Light
853
+ </DropdownMenuItem>
854
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
855
+ Dark
856
+ </DropdownMenuItem>
857
+ <DropdownMenuItem onClick={() => setTheme("system")}>
858
+ System
859
+ </DropdownMenuItem>
860
+ </DropdownMenuContent>
861
+ </DropdownMenu>
862
+ );
863
+ }
864
+ `;
865
+ const appSidebar = `"use client";
866
+
867
+ import {
868
+ FileJsonIcon,
869
+ LayoutDashboardIcon,
870
+ ListTodoIcon,
871
+ LogOutIcon,
872
+ SettingsIcon,
873
+ WrenchIcon,
874
+ } from "lucide-react";
875
+ import Link from "next/link";
876
+ import { usePathname, useRouter } from "next/navigation";
877
+ import { authClient } from "@/client/auth-client";
878
+ import { Button } from "@/components/ui/button";
879
+ import { Separator } from "@/components/ui/separator";
880
+ import { cn } from "@/lib/utils";
881
+
882
+ const navLinks = [
883
+ { href: "/dashboard", label: "Dashboard", icon: LayoutDashboardIcon },
884
+ { href: "/todos", label: "Todos", icon: ListTodoIcon },
885
+ { href: "/settings", label: "Settings", icon: SettingsIcon },
886
+ ];
887
+
888
+ const linkClassName =
889
+ "flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground";
890
+
891
+ export function AppSidebar({
892
+ user,
893
+ showDevtools,
894
+ showOpenapi,
895
+ }: {
896
+ user: { name: string; email: string };
897
+ showDevtools: boolean;
898
+ showOpenapi: boolean;
899
+ }) {
900
+ const pathname = usePathname();
901
+ const router = useRouter();
902
+
903
+ async function signOut() {
904
+ await authClient.signOut();
905
+ router.push("/");
906
+ router.refresh();
907
+ }
908
+
909
+ return (
910
+ <aside className="sticky top-0 flex h-svh w-60 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground">
911
+ <div className="px-5 py-4">
912
+ <Link href="/dashboard" className="font-heading text-sm font-semibold">
913
+ Beignet app
914
+ </Link>
915
+ </div>
916
+ <nav className="flex flex-1 flex-col gap-1 px-3" aria-label="Main">
917
+ {navLinks.map((link) => (
918
+ <Link
919
+ key={link.href}
920
+ href={link.href}
921
+ className={cn(
922
+ linkClassName,
923
+ (pathname === link.href ||
924
+ pathname.startsWith(\`\${link.href}/\`)) &&
925
+ "bg-sidebar-accent text-sidebar-accent-foreground",
926
+ )}
927
+ >
928
+ <link.icon className="size-4" />
929
+ {link.label}
930
+ </Link>
931
+ ))}
932
+ {showDevtools || showOpenapi ? <Separator className="my-2" /> : null}
933
+ {showDevtools ? (
934
+ <a href="/api/devtools" className={linkClassName}>
935
+ <WrenchIcon className="size-4" />
936
+ Devtools
937
+ </a>
938
+ ) : null}
939
+ {showOpenapi ? (
940
+ <a href="/api/openapi" className={linkClassName}>
941
+ <FileJsonIcon className="size-4" />
942
+ OpenAPI
943
+ </a>
944
+ ) : null}
945
+ </nav>
946
+ <div className="flex items-center gap-2 border-t p-3">
947
+ <div className="min-w-0 flex-1">
948
+ <p className="truncate text-sm font-medium">{user.name}</p>
949
+ <p className="truncate text-xs text-muted-foreground">{user.email}</p>
950
+ </div>
951
+ <Button
952
+ type="button"
953
+ variant="ghost"
954
+ size="icon"
955
+ aria-label="Sign out"
956
+ onClick={signOut}
957
+ >
958
+ <LogOutIcon />
959
+ </Button>
960
+ </div>
961
+ </aside>
962
+ );
963
+ }
964
+ `;
965
+ const settingsNav = `"use client";
966
+
967
+ import Link from "next/link";
968
+ import { usePathname } from "next/navigation";
969
+ import { cn } from "@/lib/utils";
970
+
971
+ const settingsLinks = [
972
+ { href: "/settings", label: "Profile" },
973
+ { href: "/settings/password", label: "Password" },
974
+ { href: "/settings/appearance", label: "Appearance" },
975
+ { href: "/settings/account", label: "Account" },
976
+ ];
977
+
978
+ export function SettingsNav() {
979
+ const pathname = usePathname();
980
+
981
+ return (
982
+ <nav className="flex flex-wrap gap-1" aria-label="Settings">
983
+ {settingsLinks.map((link) => (
984
+ <Link
985
+ key={link.href}
986
+ href={link.href}
987
+ className={cn(
988
+ "rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
989
+ pathname === link.href && "bg-muted text-foreground",
990
+ )}
991
+ >
992
+ {link.label}
993
+ </Link>
994
+ ))}
995
+ </nav>
996
+ );
997
+ }
998
+ `;
999
+ const todoApp = `"use client";
1000
+
1001
+ import { rootFormError } from "@beignet/react-hook-form";
1002
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1003
+ import { Trash2Icon } from "lucide-react";
1004
+ import { rq } from "@/client";
1005
+ import { rhf } from "@/client/forms";
1006
+ import { Button } from "@/components/ui/button";
1007
+ import { Card, CardContent } from "@/components/ui/card";
1008
+ import { Checkbox } from "@/components/ui/checkbox";
1009
+ import { Input } from "@/components/ui/input";
1010
+ import { Label } from "@/components/ui/label";
1011
+ import {
1012
+ createTodo,
1013
+ deleteTodo,
1014
+ listTodos,
1015
+ updateTodo,
1016
+ } from "@/features/todos/contracts";
1017
+ import { cn } from "@/lib/utils";
1018
+
1019
+ const createTodoForm = rhf(createTodo);
1020
+
1021
+ export function TodoApp() {
1022
+ const queryClient = useQueryClient();
1023
+ const todosQuery = useQuery(rq(listTodos).queryOptions());
1024
+ const form = createTodoForm.useForm({
1025
+ defaultValues: { title: "" },
1026
+ });
1027
+
1028
+ const invalidateTodos = () => rq(listTodos).invalidate(queryClient);
1029
+
1030
+ const createTodoMutation = useMutation(
1031
+ rq(createTodo).mutationOptions({
1032
+ onSuccess: async () => {
1033
+ form.reset();
1034
+ await invalidateTodos();
1035
+ },
1036
+ onError: (error) => {
1037
+ form.setError("root", rootFormError(error, "Could not create the todo."));
1038
+ },
1039
+ }),
1040
+ );
1041
+ const updateTodoMutation = useMutation(
1042
+ rq(updateTodo).mutationOptions({
1043
+ onSuccess: invalidateTodos,
1044
+ }),
1045
+ );
1046
+ const deleteTodoMutation = useMutation(
1047
+ rq(deleteTodo).mutationOptions({
1048
+ onSuccess: invalidateTodos,
1049
+ }),
1050
+ );
1051
+
1052
+ const onSubmit = form.handleSubmit((body) => {
1053
+ form.clearErrors("root");
1054
+ createTodoMutation.mutate({ body });
1055
+ });
1056
+
1057
+ const todos = todosQuery.data?.items ?? [];
1058
+
1059
+ return (
1060
+ <section className="flex flex-col gap-4">
1061
+ <form className="flex flex-col gap-2" onSubmit={onSubmit}>
1062
+ <Label htmlFor="todo-title">New todo</Label>
1063
+ <div className="flex gap-2">
1064
+ <Input
1065
+ id="todo-title"
1066
+ autoComplete="off"
1067
+ placeholder="Ship something type-safe"
1068
+ {...form.register("title")}
1069
+ />
1070
+ <Button type="submit" disabled={createTodoMutation.isPending}>
1071
+ {createTodoMutation.isPending ? "Adding..." : "Add"}
1072
+ </Button>
1073
+ </div>
1074
+ {form.formState.errors.title ? (
1075
+ <p className="text-sm text-destructive">
1076
+ {form.formState.errors.title.message}
1077
+ </p>
1078
+ ) : null}
1079
+ {form.formState.errors.root ? (
1080
+ <p className="text-sm text-destructive">
1081
+ {form.formState.errors.root.message}
1082
+ </p>
1083
+ ) : null}
1084
+ </form>
1085
+
1086
+ <Card className="py-0">
1087
+ <CardContent className="px-0">
1088
+ {todosQuery.isPending ? (
1089
+ <p className="px-4 py-6 text-sm text-muted-foreground">
1090
+ Loading todos...
1091
+ </p>
1092
+ ) : todosQuery.isError ? (
1093
+ <p className="px-4 py-6 text-sm text-destructive">
1094
+ Could not load todos.
1095
+ </p>
1096
+ ) : todos.length === 0 ? (
1097
+ <p className="px-4 py-6 text-sm text-muted-foreground">
1098
+ Nothing here yet — add your first todo.
1099
+ </p>
1100
+ ) : (
1101
+ <ul className="divide-y">
1102
+ {todos.map((todo) => (
1103
+ <li key={todo.id} className="flex items-center gap-3 px-4 py-3">
1104
+ <Checkbox
1105
+ id={\`todo-\${todo.id}\`}
1106
+ checked={todo.completed}
1107
+ disabled={updateTodoMutation.isPending}
1108
+ onCheckedChange={(checked) =>
1109
+ updateTodoMutation.mutate({
1110
+ path: { id: todo.id },
1111
+ body: { completed: checked === true },
1112
+ })
1113
+ }
1114
+ />
1115
+ <label
1116
+ htmlFor={\`todo-\${todo.id}\`}
1117
+ className={cn(
1118
+ "flex-1 text-sm",
1119
+ todo.completed && "text-muted-foreground line-through",
1120
+ )}
1121
+ >
1122
+ {todo.title}
1123
+ </label>
1124
+ <Button
1125
+ type="button"
1126
+ variant="ghost"
1127
+ size="icon-sm"
1128
+ aria-label="Delete todo"
1129
+ disabled={deleteTodoMutation.isPending}
1130
+ onClick={() =>
1131
+ deleteTodoMutation.mutate({ path: { id: todo.id } })
1132
+ }
1133
+ >
1134
+ <Trash2Icon />
1135
+ </Button>
1136
+ </li>
1137
+ ))}
1138
+ </ul>
1139
+ )}
1140
+ </CardContent>
1141
+ </Card>
1142
+ </section>
1143
+ );
1144
+ }
1145
+ `;
1146
+ export function shellTemplateFiles(ctx) {
1147
+ return [
1148
+ { path: "app/layout.tsx", content: rootLayout },
1149
+ { path: "app/providers.tsx", content: appProviders },
1150
+ { path: "app/page.tsx", content: landingPage },
1151
+ { path: "app/(auth)/layout.tsx", content: authLayout },
1152
+ { path: "app/(auth)/sign-in/page.tsx", content: signInPage },
1153
+ { path: "app/(auth)/sign-up/page.tsx", content: signUpPage },
1154
+ { path: "app/(app)/layout.tsx", content: appLayout },
1155
+ { path: "app/(app)/dashboard/page.tsx", content: dashboardPage(ctx) },
1156
+ { path: "app/(app)/todos/page.tsx", content: todosPage },
1157
+ { path: "app/(app)/settings/layout.tsx", content: settingsLayout },
1158
+ { path: "app/(app)/settings/page.tsx", content: settingsProfilePage },
1159
+ {
1160
+ path: "app/(app)/settings/password/page.tsx",
1161
+ content: settingsPasswordPage,
1162
+ },
1163
+ {
1164
+ path: "app/(app)/settings/appearance/page.tsx",
1165
+ content: settingsAppearancePage,
1166
+ },
1167
+ {
1168
+ path: "app/(app)/settings/account/page.tsx",
1169
+ content: settingsAccountPage,
1170
+ },
1171
+ { path: "client/index.ts", content: clientIndex },
1172
+ { path: "client/auth-client.ts", content: authClientFile },
1173
+ { path: "client/forms.ts", content: formsClient },
1174
+ { path: "components/theme-provider.tsx", content: themeProvider },
1175
+ { path: "components/theme-toggle.tsx", content: themeToggle },
1176
+ { path: "components/app-sidebar.tsx", content: appSidebar },
1177
+ { path: "components/settings-nav.tsx", content: settingsNav },
1178
+ {
1179
+ path: "features/todos/components/todo-app.tsx",
1180
+ content: todoApp,
1181
+ },
1182
+ ];
1183
+ }
1184
+ export const shellDependencies = {
1185
+ "better-auth": "1.6.11",
1186
+ "lucide-react": "^1.16.0",
1187
+ "next-themes": "^0.4.6",
1188
+ };
1189
+ export const shellDevDependencies = {};
1190
+ //# sourceMappingURL=shell.js.map