@beignet/cli 0.0.26 → 0.0.28

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 (97) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +59 -15
  3. package/dist/check.d.ts +61 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +164 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/choices.d.ts +8 -0
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +14 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/db.d.ts +8 -0
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +4 -4
  14. package/dist/db.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +156 -21
  17. package/dist/index.js.map +1 -1
  18. package/dist/inspect.d.ts.map +1 -1
  19. package/dist/inspect.js +121 -3
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/make/inbox.d.ts +63 -0
  22. package/dist/make/inbox.d.ts.map +1 -0
  23. package/dist/make/inbox.js +1747 -0
  24. package/dist/make/inbox.js.map +1 -0
  25. package/dist/make/payments.d.ts +61 -0
  26. package/dist/make/payments.d.ts.map +1 -0
  27. package/dist/make/payments.js +1900 -0
  28. package/dist/make/payments.js.map +1 -0
  29. package/dist/make/shared.d.ts +592 -0
  30. package/dist/make/shared.d.ts.map +1 -0
  31. package/dist/make/shared.js +2233 -0
  32. package/dist/make/shared.js.map +1 -0
  33. package/dist/make/tenancy.d.ts +101 -0
  34. package/dist/make/tenancy.d.ts.map +1 -0
  35. package/dist/make/tenancy.js +4409 -0
  36. package/dist/make/tenancy.js.map +1 -0
  37. package/dist/make.d.ts +8 -44
  38. package/dist/make.d.ts.map +1 -1
  39. package/dist/make.js +57 -3689
  40. package/dist/make.js.map +1 -1
  41. package/dist/mcp.d.ts.map +1 -1
  42. package/dist/mcp.js +45 -4
  43. package/dist/mcp.js.map +1 -1
  44. package/dist/provider-add.d.ts +19 -0
  45. package/dist/provider-add.d.ts.map +1 -0
  46. package/dist/provider-add.js +691 -0
  47. package/dist/provider-add.js.map +1 -0
  48. package/dist/provider-audit.d.ts.map +1 -1
  49. package/dist/provider-audit.js +77 -1
  50. package/dist/provider-audit.js.map +1 -1
  51. package/dist/registry-edits.d.ts +9 -0
  52. package/dist/registry-edits.d.ts.map +1 -1
  53. package/dist/registry-edits.js +32 -0
  54. package/dist/registry-edits.js.map +1 -1
  55. package/dist/task.d.ts +2 -0
  56. package/dist/task.d.ts.map +1 -1
  57. package/dist/task.js +2 -0
  58. package/dist/task.js.map +1 -1
  59. package/dist/templates/agents.d.ts.map +1 -1
  60. package/dist/templates/agents.js +29 -11
  61. package/dist/templates/agents.js.map +1 -1
  62. package/dist/templates/base.d.ts.map +1 -1
  63. package/dist/templates/base.js +42 -12
  64. package/dist/templates/base.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +7 -1
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/server.d.ts.map +1 -1
  69. package/dist/templates/server.js +44 -37
  70. package/dist/templates/server.js.map +1 -1
  71. package/dist/templates/shared.d.ts +5 -0
  72. package/dist/templates/shared.d.ts.map +1 -1
  73. package/dist/templates/shared.js +5 -0
  74. package/dist/templates/shared.js.map +1 -1
  75. package/package.json +2 -2
  76. package/skills/app-structure/SKILL.md +1 -1
  77. package/src/check.ts +246 -0
  78. package/src/choices.ts +28 -0
  79. package/src/db.ts +4 -4
  80. package/src/index.ts +232 -21
  81. package/src/inspect.ts +168 -2
  82. package/src/make/inbox.ts +2015 -0
  83. package/src/make/payments.ts +2182 -0
  84. package/src/make/shared.ts +3638 -0
  85. package/src/make/tenancy.ts +4809 -0
  86. package/src/make.ts +388 -5355
  87. package/src/mcp.ts +66 -3
  88. package/src/provider-add.ts +926 -0
  89. package/src/provider-audit.ts +95 -3
  90. package/src/registry-edits.ts +47 -0
  91. package/src/task.ts +8 -10
  92. package/src/templates/agents.ts +29 -11
  93. package/src/templates/base.ts +43 -12
  94. package/src/templates/db/sqlite.ts +7 -1
  95. package/src/templates/server.ts +44 -37
  96. package/src/templates/shared.ts +5 -0
  97. package/src/test-helpers/generated-app.ts +168 -0
@@ -365,9 +365,101 @@ export async function readProviderListEntries(
365
365
  )
366
366
  ).join("\n");
367
367
  const providerListSource = extractProviderListSource(providerSource);
368
- return providerListSource === undefined
369
- ? []
370
- : providerListEntries(providerListSource);
368
+ const entries =
369
+ providerListSource === undefined
370
+ ? []
371
+ : providerListEntries(providerListSource);
372
+ return expandProviderEntryIdentifiers(entries, providerSource);
373
+ }
374
+
375
+ /**
376
+ * Resolve identifiers referenced by provider list entries to their `const`
377
+ * initializers in the same providers module.
378
+ *
379
+ * Registration detection matches provider tokens against entry text, so an
380
+ * environment-switched provider extracted to a named constant —
381
+ * `const storage = env.TOKEN ? blobProvider : createLocalStorageProvider()`
382
+ * registered as `[storage]` — must contribute its initializer text too, not
383
+ * just the identifier. Resolution follows chains through a seen-set so
384
+ * self-referential constants cannot loop.
385
+ */
386
+ function expandProviderEntryIdentifiers(
387
+ entries: string[],
388
+ source: string,
389
+ ): string[] {
390
+ const expanded = [...entries];
391
+ const seen = new Set<string>();
392
+ const queue = [...entries];
393
+
394
+ while (queue.length > 0) {
395
+ const entry = queue.pop() as string;
396
+ for (const match of entry.matchAll(/\b[A-Za-z_$][\w$]*\b/g)) {
397
+ const name = match[0];
398
+ if (seen.has(name)) continue;
399
+ seen.add(name);
400
+
401
+ const initializer = constInitializerText(source, name);
402
+ if (initializer !== undefined) {
403
+ expanded.push(initializer);
404
+ queue.push(initializer);
405
+ }
406
+ }
407
+ }
408
+
409
+ return expanded;
410
+ }
411
+
412
+ /**
413
+ * Extract the initializer text of `const <name> = ...;`, ending at the first
414
+ * top-level semicolon outside strings and brackets.
415
+ */
416
+ function constInitializerText(
417
+ source: string,
418
+ name: string,
419
+ ): string | undefined {
420
+ const declaration = new RegExp(`\\bconst\\s+${name}\\s*=`).exec(source);
421
+ if (!declaration) return undefined;
422
+
423
+ const start = declaration.index + declaration[0].length;
424
+ let depth = 0;
425
+ let quote: '"' | "'" | "`" | undefined;
426
+ let escaped = false;
427
+
428
+ for (let index = start; index < source.length; index += 1) {
429
+ const char = source[index];
430
+
431
+ if (quote) {
432
+ if (escaped) {
433
+ escaped = false;
434
+ } else if (char === "\\") {
435
+ escaped = true;
436
+ } else if (char === quote) {
437
+ quote = undefined;
438
+ }
439
+ continue;
440
+ }
441
+
442
+ if (char === '"' || char === "'" || char === "`") {
443
+ quote = char;
444
+ continue;
445
+ }
446
+
447
+ if (char === "(" || char === "[" || char === "{") {
448
+ depth += 1;
449
+ continue;
450
+ }
451
+
452
+ if (char === ")" || char === "]" || char === "}") {
453
+ depth -= 1;
454
+ continue;
455
+ }
456
+
457
+ if (char === ";" && depth === 0) {
458
+ return source.slice(start, index).trim();
459
+ }
460
+ }
461
+
462
+ return source.slice(start).trim() || undefined;
371
463
  }
372
464
 
373
465
  export function registrationTokenHint(tokens: readonly string[]): string {
@@ -94,6 +94,53 @@ export function insertAfterImports(source: string, line: string): string {
94
94
  return lines.join("\n");
95
95
  }
96
96
 
97
+ /**
98
+ * Add a name to an `import { ... } from "<module>";` value import, creating
99
+ * the import when the module is not imported yet. Names are inserted in
100
+ * sorted position and single-line versus multiline formatting is preserved.
101
+ *
102
+ * Mirrors the `addNamedTypeImport` helper for value imports; type-only
103
+ * imports (`import type { ... }`) from the same module are left untouched.
104
+ */
105
+ export function addNamedValueImport(
106
+ source: string,
107
+ name: string,
108
+ moduleName: string,
109
+ ): string {
110
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ const importPattern = new RegExp(
112
+ `import \\{([^}]*)\\} from "${escapedModule}";`,
113
+ );
114
+ const match = importPattern.exec(source);
115
+ if (!match) {
116
+ return insertAfterImports(
117
+ source,
118
+ `import { ${name} } from "${moduleName}";`,
119
+ );
120
+ }
121
+
122
+ const names = match[1]
123
+ .split(",")
124
+ .map((entry) => entry.trim())
125
+ .filter(Boolean);
126
+ if (names.includes(name)) return source;
127
+
128
+ const insertIndex = names.findIndex(
129
+ (existing) => existing.localeCompare(name) > 0,
130
+ );
131
+ if (insertIndex === -1) names.push(name);
132
+ else names.splice(insertIndex, 0, name);
133
+
134
+ const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
135
+ const replacement = match[1].includes("\n")
136
+ ? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
137
+ : `import { ${names.join(", ")} } from "${moduleName}";`;
138
+
139
+ return `${source.slice(0, match.index)}${replacement}${source.slice(
140
+ match.index + match[0].length,
141
+ )}`;
142
+ }
143
+
97
144
  /**
98
145
  * Collect every identifier referenced inside an array (or object) expression.
99
146
  */
package/src/task.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { TaskDef } from "@beignet/core/tasks";
3
+ import type { TaskDef, TaskRunContextArgs } from "@beignet/core/tasks";
4
4
  import { createJiti } from "jiti";
5
5
  import { loadBeignetConfig, normalizePath } from "./config.js";
6
6
 
@@ -11,6 +11,7 @@ export type RunAppTaskOptions = {
11
11
  name: string;
12
12
  cwd?: string;
13
13
  input?: string | Record<string, unknown>;
14
+ tenant?: string;
14
15
  modulePath?: string;
15
16
  };
16
17
 
@@ -23,20 +24,15 @@ export type RunAppTaskResult = {
23
24
  cwd: string;
24
25
  modulePath: string;
25
26
  input: unknown;
27
+ tenant?: string;
26
28
  output: unknown;
27
29
  durationMs: number;
28
30
  };
29
31
 
30
- type TaskContextArgs = {
31
- task: TaskDef;
32
- taskName: string;
33
- input: unknown;
34
- };
35
-
36
32
  type TaskModule = {
37
33
  tasks?: readonly unknown[];
38
- createTaskContext?: (args: TaskContextArgs) => unknown;
39
- stopTaskContext?: (ctx: unknown, args: TaskContextArgs) => unknown;
34
+ createTaskContext?: (args: TaskRunContextArgs) => unknown;
35
+ stopTaskContext?: (ctx: unknown, args: TaskRunContextArgs) => unknown;
40
36
  };
41
37
 
42
38
  /**
@@ -56,10 +52,11 @@ export async function runAppTask(
56
52
  // `beignet lint`, work without a built @beignet/core.
57
53
  const { parseTaskInput } = await import("@beignet/core/tasks");
58
54
  const parsedInput = await parseTaskInput(task, rawInput);
59
- const contextArgs = {
55
+ const contextArgs: TaskRunContextArgs = {
60
56
  task,
61
57
  taskName: task.name,
62
58
  input: parsedInput,
59
+ tenant: options.tenant,
63
60
  };
64
61
  const ctx = taskModule.createTaskContext
65
62
  ? await taskModule.createTaskContext(contextArgs)
@@ -78,6 +75,7 @@ export async function runAppTask(
78
75
  cwd,
79
76
  modulePath,
80
77
  input: parsedInput,
78
+ ...(options.tenant !== undefined ? { tenant: options.tenant } : {}),
81
79
  output,
82
80
  durationMs: Math.round(performance.now() - startedAt),
83
81
  };
@@ -25,6 +25,9 @@ export function agentsMd(ctx: TemplateContext): string {
25
25
  ? "npm run typecheck"
26
26
  : `${ctx.packageManager} run typecheck`;
27
27
  const intent = intentRunner(ctx);
28
+ const capabilityClientRow = ctx.api
29
+ ? ""
30
+ : "\n| Query cache keys and invalidation | `rq(contract).invalidate(queryClient, params?)`, `.key(...)`, `.filter(...)` from `client/` (`@beignet/react-query`). Thin named wrappers in `features/<feature>/client/` are the convention — hand-built key arrays are not. |";
28
31
  const specificSkills = ctx.api
29
32
  ? "`@beignet/next#routes-server`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
30
33
  : "`@beignet/next#routes-server`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
@@ -56,8 +59,7 @@ use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
56
59
  \`${cli} make seed\`, and \`${cli} make upload\` create or update their
57
60
  required app entrypoints.
58
61
  \`${cli} doctor\` detects registration drift. \`${cli} doctor --fix\` repairs
59
- route-group, schedule, task, and outbox registration; listener drift is
60
- report-only and must be fixed by hand.
62
+ route-group, schedule, task, outbox, and listener registration.
61
63
 
62
64
  ## Prefer generators
63
65
 
@@ -68,20 +70,36 @@ need a richer reference slice with policy, client helpers, workflow artifacts,
68
70
  events, listener registration, jobs, and outbox wiring. After changing the Drizzle schema in
69
71
  \`infra/db/schema/\`, run \`${cli} db generate\` then \`${cli} db migrate\`.
70
72
 
73
+ ## The framework already solves these
74
+
75
+ Check this table before hand-rolling. Every row is an existing API that
76
+ apps have reimplemented by accident:
77
+
78
+ | Need | Use |
79
+ | --- | --- |${capabilityClientRow}
80
+ | App context outside HTTP — agents, queues, scripts, backfills | \`server.createServiceContext(input)\`, or \`server.runServiceContext(input, fn)\` in one-off scripts. Never hand-assemble a context or call \`gate.attach(...)\` — the server owns gate attachment. |
81
+ | Ports outside a request — auth callbacks, module-level helpers | \`const { ports } = await getServer()\` (dynamic \`import("@/server")\` breaks module cycles). Do not construct parallel provider clients or fall back to \`console.*\` when \`ports.logger\` exists. |
82
+ | Routes that cannot be contracts — webhooks, third-party callbacks, streaming | \`createWebhookRoute\`, \`createPaymentWebhookRoute\`, \`createScheduleRoute\`, \`createOutboxDrainRoute\` from \`@beignet/next\`; \`server.rawRoute(...)\` for anything else. All run the hooks pipeline — never hand-enforce rate limits in a route body. |
83
+ | Rate limiting or idempotency on a route | Declare \`metadata.rateLimit\` / \`metadata.idempotency\` on the contract (or the \`pipeline\` option on raw routes); hooks enforce it. |
84
+ | Route-level tests that exercise real hooks | \`createTestApp\` / \`createTestRequester\` from \`@beignet/web/testing\`. Bind the rate-limit and idempotency ports in the test app when asserting 429s or replay. |
85
+ | Environment configuration | \`lib/env.ts\` (\`createEnv\`), never ad-hoc \`process.env\` reads in app code. |
86
+ | Request feels slow | Open devtools; every request span breaks down into per-stage bars (context creation is the usual serverless culprit). |
87
+
71
88
  ## Validation loop
72
89
 
73
90
  Run after every change:
74
91
 
75
92
  \`\`\`bash
76
- ${lint}
77
- ${cli} lint
78
- ${cli} doctor --strict
79
- ${test}
80
- ${typecheck}
93
+ ${cli} check
81
94
  \`\`\`
82
95
 
83
- \`${lint}\` runs Biome's code lint. Use \`${format}\` to apply formatting.
84
- \`${cli} lint\` is Beignet's dependency-direction lint.
96
+ It runs \`${cli} lint\` (Beignet's dependency-direction lint),
97
+ \`${cli} doctor --strict\`, and the app's \`lint\` (Biome), \`typecheck\`, and
98
+ \`test\` scripts in one pass, reporting every failure. Add \`--fix\` to apply
99
+ doctor's low-risk registration fixes first. Use \`${format}\` to apply
100
+ formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
101
+ \`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
102
+ need one check alone.
85
103
 
86
104
  ## Package skills
87
105
 
@@ -118,9 +136,9 @@ skill-loading block.
118
136
  ## MCP server
119
137
 
120
138
  \`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
121
- \`./node_modules/.bin/beignet mcp\`, which exposes routes/doctor/lint/make as
139
+ \`./node_modules/.bin/beignet mcp\`, which exposes routes/doctor/lint/make/provider_add as
122
140
  structured tools named exactly: \`routes\`, \`doctor\`, \`doctor_fix\`,
123
- \`lint\`, \`make\`. Clients that do not read \`.mcp.json\` can use the same
141
+ \`lint\`, \`make\`, \`provider_add\`. Clients that do not read \`.mcp.json\` can use the same
124
142
  command from the app root; use \`${cli} mcp\` only for terminal debugging.
125
143
  `;
126
144
  }
@@ -209,6 +209,41 @@ The starter's shipped tests use in-memory ports and need no database server. Dat
209
209
  `
210
210
  : "";
211
211
 
212
+ const optionalAuthRedirects = ctx.api
213
+ ? ""
214
+ : `## Optional faster auth redirects
215
+
216
+ The starter keeps real auth enforcement in \`app/(app)/layout.tsx\`,
217
+ \`server/context.ts\`, route hooks, and protected use cases. After your product
218
+ routes settle, you can add a root \`proxy.ts\` to redirect requests that do not
219
+ have a Better Auth session cookie before rendering the protected app shell:
220
+
221
+ \`\`\`ts
222
+ import { getSessionCookie } from "better-auth/cookies";
223
+ import { NextResponse, type NextRequest } from "next/server";
224
+
225
+ export function proxy(request: NextRequest) {
226
+ const sessionCookie = getSessionCookie(request);
227
+
228
+ if (!sessionCookie) {
229
+ return NextResponse.redirect(new URL("/sign-in", request.url));
230
+ }
231
+
232
+ return NextResponse.next();
233
+ }
234
+
235
+ export const config = {
236
+ matcher: ["/dashboard/:path*", "/todos/:path*", "/settings/:path*"],
237
+ };
238
+ \`\`\`
239
+
240
+ This is only an optimistic UX gate. A session cookie can be absent, stale, or
241
+ manually created, so keep the layout/session checks and Beignet auth
242
+ hooks/use-case helpers. Update the matcher to match your app's protected URLs;
243
+ route groups such as \`app/(app)\` are not part of the URL.
244
+
245
+ `;
246
+
212
247
  return `# ${ctx.name}
213
248
 
214
249
  Beignet app scaffolded with \`@beignet/cli\`.
@@ -241,15 +276,14 @@ ${firstOpen}
241
276
  \`\`\`bash
242
277
  # in another terminal
243
278
  ${cli} routes
244
- ${lint}
245
- ${cli} lint
246
- ${cli} doctor
247
- ${test}
248
- ${typecheck}
279
+ ${cli} check
249
280
  \`\`\`
250
281
 
251
- \`routes\` shows the contracts Beignet can inspect. \`${cli} lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
252
- \`${lint}\` runs Biome over the starter; use \`${format}\` to apply formatting.
282
+ \`routes\` shows the contracts Beignet can inspect. \`check\` runs the whole
283
+ validation loop in one pass: \`${cli} lint\` (dependency direction),
284
+ \`${cli} doctor --strict\` (route, OpenAPI, and resource drift), and the
285
+ \`lint\`, \`typecheck\`, and \`test\` package scripts. Use \`${format}\` to
286
+ apply Biome formatting.
253
287
  ${testingNotes}
254
288
  ## Coding agents
255
289
 
@@ -272,11 +306,7 @@ ${start}
272
306
  ${cli} make feature projects
273
307
  ${cli} db generate
274
308
  ${cli} db migrate
275
- ${test}
276
- ${lint}
277
- ${typecheck}
278
- ${cli} lint
279
- ${cli} doctor
309
+ ${cli} check
280
310
  \`\`\`
281
311
 
282
312
  \`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate and migrate the database before running the app against the new feature.
@@ -306,6 +336,7 @@ Use \`${cli} make feature projects --recipe full-slice\` when you want a richer
306
336
  - \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
307
337
  - \`lib/better-auth.ts\` owns Better Auth setup and keeps provider-specific auth details outside use cases.
308
338
  ${shellMap}
339
+ ${optionalAuthRedirects}
309
340
  ## Before deploying
310
341
 
311
342
  - ${
@@ -225,7 +225,13 @@ export function ensureDatabaseReady(client: Client): Promise<void> {
225
225
  }
226
226
 
227
227
  async function prepare(client: Client): Promise<void> {
228
- await client.execute("PRAGMA busy_timeout = 5000");
228
+ // Remote libsql (Turso) runs over HTTP with no file locking and rejects
229
+ // the lock-wait pragma, so it only applies to local file databases.
230
+ if (client.protocol === "file") {
231
+ await client.execute("PRAGMA busy_timeout = 5000");
232
+ }
233
+ // SQLite leaves foreign keys off per connection unless asked.
234
+ await client.execute("PRAGMA foreign_keys = ON");
229
235
 
230
236
  if (process.env.NODE_ENV === "production") {
231
237
  const tables = await client.execute(
@@ -296,16 +296,18 @@ export function server(ctx: TemplateContext): string {
296
296
  \t\t},`;
297
297
 
298
298
  return `import "@beignet/core/server-only";
299
- import { createNextServer } from "@beignet/next";
299
+ import { createNextServer, createNextServerLoader } from "@beignet/next";
300
300
  import { createErrorReportingHooks, createIdempotencyHooks } from "@beignet/core/server";
301
301
  import type { AppContext } from "@/app-context";
302
302
  import { appPorts } from "@/infra/app-ports";
303
303
  import { env } from "@/lib/env";
304
304
  import { appContext } from "./context";
305
- import { providers } from "./providers";
306
305
  import { routes } from "./routes";
307
306
 
308
- export const server = await createNextServer({
307
+ export const getServer = createNextServerLoader(async () => {
308
+ const { providers } = await import("./providers");
309
+
310
+ return createNextServer({
309
311
  ports: appPorts,
310
312
  providers,
311
313
  providerConfig: {
@@ -333,6 +335,7 @@ ${providerConfigEntries}
333
335
  },
334
336
  };
335
337
  },
338
+ });
336
339
  });
337
340
  `;
338
341
  }
@@ -557,15 +560,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
557
560
  return typeof value === "object" && value !== null;
558
561
  }
559
562
  `,
560
- apiCatchAllRoute: `import { server } from "@/server";
561
-
562
- export const GET = server.api;
563
- export const HEAD = server.api;
564
- export const OPTIONS = server.api;
565
- export const PATCH = server.api;
566
- export const POST = server.api;
567
- export const PUT = server.api;
568
- export const DELETE = server.api;
563
+ apiCatchAllRoute: `import { createApiRoute } from "@beignet/next";
564
+ import { getServer } from "@/server";
565
+
566
+ export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
567
+ createApiRoute(getServer);
569
568
  `,
570
569
  healthRoute: `import { env } from "@/lib/env";
571
570
 
@@ -584,12 +583,12 @@ export function GET() {
584
583
  );
585
584
  }
586
585
  `,
587
- readyRoute: `import { createHealthHandler } from "@beignet/core/server";
586
+ readyRoute: `import { createHealthRoute } from "@beignet/next";
588
587
  import { env } from "@/lib/env";
589
- import { server } from "@/server";
588
+ import { getServer } from "@/server";
590
589
 
591
- const readiness = createHealthHandler(
592
- server.ports,
590
+ export const { GET } = createHealthRoute(
591
+ getServer,
593
592
  {
594
593
  checks: {
595
594
  database: (ports) => ports.db.checkHealth(),
@@ -598,42 +597,50 @@ const readiness = createHealthHandler(
598
597
  },
599
598
  env.NODE_ENV,
600
599
  );
601
-
602
- export async function GET(request: Request) {
603
- const response = await readiness(request);
604
-
605
- return Response.json(response.body, {
606
- status: response.status,
607
- headers: {
608
- ...response.headers,
609
- "cache-control": "no-store",
610
- },
611
- });
612
- }
613
600
  `,
614
601
  devtoolsRoute: `import { createDevtoolsRoute } from "@beignet/devtools";
615
602
  import { env } from "@/lib/env";
616
- import { server } from "@/server";
603
+ import { getServer } from "@/server";
617
604
 
618
- export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
619
- basePath: "/api/devtools",
620
- enabled: env.DEVTOOLS_ENABLED,
621
- });
605
+ export const { GET, POST } = createDevtoolsRoute(
606
+ async () => (await getServer()).ports.devtools,
607
+ {
608
+ basePath: "/api/devtools",
609
+ enabled: env.DEVTOOLS_ENABLED,
610
+ },
611
+ );
622
612
  `,
623
613
  openApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
624
614
  import { env } from "@/lib/env";
625
- import { server } from "@/server";
615
+ import { contracts } from "@/server/routes";
626
616
 
627
- export const GET = createOpenAPIHandler(server.contracts, {
617
+ export const GET = createOpenAPIHandler(contracts, {
628
618
  title: "Beignet starter API",
629
619
  version: "0.1.0",
630
620
  servers: [{ url: env.APP_URL }],
631
621
  });
632
622
  `,
633
623
  authRoute: `import { toNextJsHandler } from "better-auth/next-js";
634
- import { auth } from "@/lib/better-auth";
635
624
 
636
- export const { GET, POST } = toNextJsHandler(auth);
625
+ type AuthRouteMethod = "GET" | "POST";
626
+
627
+ async function handleAuthRoute(
628
+ method: AuthRouteMethod,
629
+ request: Request,
630
+ ): Promise<Response> {
631
+ const { auth } = await import("@/lib/better-auth");
632
+ const handlers = toNextJsHandler(auth);
633
+
634
+ return handlers[method](request);
635
+ }
636
+
637
+ export function GET(request: Request): Promise<Response> {
638
+ return handleAuthRoute("GET", request);
639
+ }
640
+
641
+ export function POST(request: Request): Promise<Response> {
642
+ return handleAuthRoute("POST", request);
643
+ }
637
644
  `,
638
645
  // API-only scaffold frontend: a minimal layout and landing page that point
639
646
  // at the contract-backed API surface, plus a typed client without React
@@ -54,7 +54,12 @@ export const externalVersions = {
54
54
  pglite: "^0.5.2",
55
55
  pino: "^9.7.0",
56
56
  resend: "^6.14.0",
57
+ sentryNode: "^10.58.0",
58
+ nodemailer: "^8.0.5",
59
+ openFeatureServerSdk: "^1.22.0",
57
60
  typesPg: "^8.11.0",
61
+ ioredis: "^5.0.0",
62
+ awsSdkS3: "^3.955.0",
58
63
  upstashRateLimit: "^2.0.0",
59
64
  upstashRedis: "^1.0.0",
60
65
  };