@beignet/cli 0.0.27 → 0.0.29

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 (63) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -6
  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 +2 -2
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +1 -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 +2 -2
  14. package/dist/db.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +47 -8
  17. package/dist/index.js.map +1 -1
  18. package/dist/inspect.d.ts.map +1 -1
  19. package/dist/inspect.js +70 -0
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/make/shared.d.ts +17 -0
  22. package/dist/make/shared.d.ts.map +1 -1
  23. package/dist/make/shared.js +67 -0
  24. package/dist/make/shared.js.map +1 -1
  25. package/dist/make.d.ts.map +1 -1
  26. package/dist/make.js +11 -55
  27. package/dist/make.js.map +1 -1
  28. package/dist/mcp.js +1 -1
  29. package/dist/mcp.js.map +1 -1
  30. package/dist/provider-add.d.ts.map +1 -1
  31. package/dist/provider-add.js +50 -0
  32. package/dist/provider-add.js.map +1 -1
  33. package/dist/provider-audit.d.ts.map +1 -1
  34. package/dist/provider-audit.js +77 -1
  35. package/dist/provider-audit.js.map +1 -1
  36. package/dist/templates/agents.d.ts.map +1 -1
  37. package/dist/templates/agents.js +27 -9
  38. package/dist/templates/agents.js.map +1 -1
  39. package/dist/templates/base.d.ts.map +1 -1
  40. package/dist/templates/base.js +42 -12
  41. package/dist/templates/base.js.map +1 -1
  42. package/dist/templates/db/sqlite.d.ts.map +1 -1
  43. package/dist/templates/db/sqlite.js +7 -1
  44. package/dist/templates/db/sqlite.js.map +1 -1
  45. package/dist/templates/shared.d.ts +1 -0
  46. package/dist/templates/shared.d.ts.map +1 -1
  47. package/dist/templates/shared.js +1 -0
  48. package/dist/templates/shared.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/check.ts +246 -0
  51. package/src/choices.ts +3 -1
  52. package/src/db.ts +2 -2
  53. package/src/index.ts +62 -8
  54. package/src/inspect.ts +100 -0
  55. package/src/make/shared.ts +88 -0
  56. package/src/make.ts +10 -68
  57. package/src/mcp.ts +1 -1
  58. package/src/provider-add.ts +51 -0
  59. package/src/provider-audit.ts +95 -3
  60. package/src/templates/agents.ts +27 -9
  61. package/src/templates/base.ts +43 -12
  62. package/src/templates/db/sqlite.ts +7 -1
  63. package/src/templates/shared.ts +1 -0
@@ -7,6 +7,7 @@ import {
7
7
  type ResolvedBeignetConfig,
8
8
  } from "../config.js";
9
9
  import {
10
+ type AppendResult,
10
11
  appendToArrayExpression,
11
12
  appendToNamedArray,
12
13
  arrayInitializerInfo,
@@ -349,6 +350,93 @@ export function providersFilePath(config: ResolvedBeignetConfig): string {
349
350
  return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
350
351
  }
351
352
 
353
+ /**
354
+ * Wire a feature listener registry into the generated providers file by
355
+ * adding a `<registry>Provider` lifecycle provider whose setup calls
356
+ * `registerListeners(...)` and registering it in the providers array.
357
+ *
358
+ * Shared by `beignet make listener` and `beignet doctor --fix` so both write
359
+ * paths produce byte-identical wiring. Returns `missing` when the exported
360
+ * providers array anchor cannot be found; callers decide whether that is a
361
+ * hard error (make) or a keep-the-diagnostic bail (doctor).
362
+ */
363
+ export function wireListenersProviderSource(
364
+ source: string,
365
+ options: {
366
+ registryName: string;
367
+ featureKebab: string;
368
+ listenerModule: string;
369
+ config: ResolvedBeignetConfig;
370
+ },
371
+ ): AppendResult {
372
+ const { registryName, featureKebab, listenerModule, config } = options;
373
+ const providerName = `${registryName}Provider`;
374
+ let next = source;
375
+ next = addNamedImport(next, "registerListeners", "@beignet/core/events");
376
+ next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
377
+ next = addNamedImport(next, "createProvider", "@beignet/core/providers");
378
+ next = addNamedTypeImport(
379
+ next,
380
+ "AppContext",
381
+ aliasModule(config.paths.appContext),
382
+ );
383
+ next = addNamedTypeImport(
384
+ next,
385
+ "AppServiceContextInput",
386
+ aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
387
+ );
388
+ next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
389
+
390
+ const listenerImport = `import { ${registryName} } from "${listenerModule}";`;
391
+ if (!next.includes(listenerImport)) {
392
+ next = insertAfterImports(next, listenerImport);
393
+ }
394
+
395
+ if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
396
+ const providerDefinition = `const ${providerName} = createProvider<
397
+ \tPick<AppPorts, "eventBus" | "logger">,
398
+ \tAppContext,
399
+ \tAppServiceContextInput
400
+ >()({
401
+ \tname: "${featureKebab}-listeners",
402
+ \tsetup({ ports, createServiceContext }) {
403
+ \t\tconst unregister = registerListeners(ports.eventBus, ${registryName}, {
404
+ \t\t\tctx: () =>
405
+ \t\t\t\tcreateServiceContext({
406
+ \t\t\t\t\tactor: createServiceActor("beignet-listener"),
407
+ \t\t\t\t}),
408
+ \t\t\tonError(error, listener) {
409
+ \t\t\t\tports.logger.error("Event listener failed", {
410
+ \t\t\t\t\terror,
411
+ \t\t\t\t\tlistenerName: listener.name,
412
+ \t\t\t\t});
413
+ \t\t\t},
414
+ \t\t});
415
+
416
+ \t\treturn {
417
+ \t\t\tstop() {
418
+ \t\t\t\tunregister();
419
+ \t\t\t},
420
+ \t\t};
421
+ \t},
422
+ });
423
+ `;
424
+ const withProvider = next.replace(
425
+ /\nexport const providers = \[/,
426
+ `\n${providerDefinition}\nexport const providers = [`,
427
+ );
428
+ if (withProvider === next) return { kind: "missing" };
429
+ next = withProvider;
430
+ }
431
+
432
+ const appended = appendToNamedArray(next, "providers", providerName);
433
+ if (appended.kind === "missing") return { kind: "missing" };
434
+ if (appended.kind === "updated") next = appended.source;
435
+
436
+ if (next === source) return { kind: "unchanged" };
437
+ return { kind: "updated", source: next };
438
+ }
439
+
352
440
  /**
353
441
  * Wire provider-backed app ports a generator depends on.
354
442
  *
package/src/make.ts CHANGED
@@ -131,6 +131,7 @@ import {
131
131
  usesFeatureOwnedPolicies,
132
132
  usesFeatureOwnedTests,
133
133
  type WriteGeneratedFileResult,
134
+ wireListenersProviderSource,
134
135
  wirePortProviders,
135
136
  writeGeneratedFile,
136
137
  writePlannedGeneratedFile,
@@ -1392,79 +1393,20 @@ async function updateListenerProviderWiring(
1392
1393
  }
1393
1394
 
1394
1395
  const listenerIndex = featureArtifactIndexFile("listener", names, config);
1395
- const providerName = `${names.featureSingularCamel}ListenersProvider`;
1396
- let next = original;
1397
- next = addNamedImport(next, "registerListeners", "@beignet/core/events");
1398
- next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
1399
- next = addNamedImport(next, "createProvider", "@beignet/core/providers");
1400
- next = addNamedTypeImport(
1401
- next,
1402
- "AppContext",
1403
- aliasModule(config.paths.appContext),
1404
- );
1405
- next = addNamedTypeImport(
1406
- next,
1407
- "AppServiceContextInput",
1408
- aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
1409
- );
1410
- next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
1411
-
1412
- const listenerImport = `import { ${listenerIndex.registryName} } from "${aliasModule(listenerIndex.path)}";`;
1413
- if (!next.includes(listenerImport)) {
1414
- next = insertAfterImports(next, listenerImport);
1415
- }
1416
-
1417
- if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
1418
- const providerDefinition = `const ${providerName} = createProvider<
1419
- \tPick<AppPorts, "eventBus" | "logger">,
1420
- \tAppContext,
1421
- \tAppServiceContextInput
1422
- >()({
1423
- \tname: "${names.feature.kebab}-listeners",
1424
- \tsetup({ ports, createServiceContext }) {
1425
- \t\tconst unregister = registerListeners(ports.eventBus, ${listenerIndex.registryName}, {
1426
- \t\t\tctx: () =>
1427
- \t\t\t\tcreateServiceContext({
1428
- \t\t\t\t\tactor: createServiceActor("beignet-listener"),
1429
- \t\t\t\t}),
1430
- \t\t\tonError(error, listener) {
1431
- \t\t\t\tports.logger.error("Event listener failed", {
1432
- \t\t\t\t\terror,
1433
- \t\t\t\t\tlistenerName: listener.name,
1434
- \t\t\t\t});
1435
- \t\t\t},
1436
- \t\t});
1437
-
1438
- \t\treturn {
1439
- \t\t\tstop() {
1440
- \t\t\t\tunregister();
1441
- \t\t\t},
1442
- \t\t};
1443
- \t},
1444
- });
1445
- `;
1446
- const withProvider = next.replace(
1447
- /\nexport const providers = \[/,
1448
- `\n${providerDefinition}\nexport const providers = [`,
1449
- );
1450
- if (withProvider === next) {
1451
- throw new Error(
1452
- `Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
1453
- );
1454
- }
1455
- next = withProvider;
1456
- }
1457
-
1458
- const appended = appendToNamedArray(next, "providers", providerName);
1459
- if (appended.kind === "missing") {
1396
+ const result = wireListenersProviderSource(original, {
1397
+ registryName: listenerIndex.registryName,
1398
+ featureKebab: names.feature.kebab,
1399
+ listenerModule: aliasModule(listenerIndex.path),
1400
+ config,
1401
+ });
1402
+ if (result.kind === "missing") {
1460
1403
  throw new Error(
1461
1404
  `Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
1462
1405
  );
1463
1406
  }
1464
- if (appended.kind === "updated") next = appended.source;
1465
1407
 
1466
- if (next === original) return "skipped";
1467
- if (!options.dryRun) await writeFile(filePath, next);
1408
+ if (result.kind === "unchanged") return "skipped";
1409
+ if (!options.dryRun) await writeFile(filePath, result.source);
1468
1410
  return "updated";
1469
1411
  }
1470
1412
 
package/src/mcp.ts CHANGED
@@ -380,7 +380,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
380
380
  "doctor_fix",
381
381
  {
382
382
  description:
383
- "Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, and outbox registration drift; listener registration drift is report-only. Returns applied fixes and remaining diagnostics as JSON.",
383
+ "Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, outbox, and listener registration drift. Returns applied fixes and remaining diagnostics as JSON.",
384
384
  inputSchema: {
385
385
  strict: z
386
386
  .boolean()
@@ -396,6 +396,57 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
396
396
  conflictingProviderEntries: [
397
397
  "localStorageProvider",
398
398
  "createLocalStorageProvider",
399
+ "vercelBlobStorageProvider",
400
+ "createVercelBlobStorageProvider",
401
+ ],
402
+ },
403
+ "vercel-blob-storage": {
404
+ name: "vercel-blob-storage",
405
+ displayName: "Vercel Blob storage",
406
+ importName: "vercelBlobStorageProvider",
407
+ importFrom: "@beignet/provider-storage-vercel-blob",
408
+ providerEntry: "vercelBlobStorageProvider",
409
+ dependencies: (packageJson) => ({
410
+ "@beignet/provider-storage-vercel-blob":
411
+ beignetDependencyVersion(packageJson),
412
+ "@vercel/blob": externalVersions.vercelBlob,
413
+ }),
414
+ appPort: {
415
+ key: "storage",
416
+ typeName: "StoragePort",
417
+ importName: "StoragePort",
418
+ importFrom: "@beignet/core/storage",
419
+ },
420
+ envVars: ["BLOB_READ_WRITE_TOKEN"],
421
+ envExample: [
422
+ "# Vercel Blob storage provider. Vercel sets BLOB_READ_WRITE_TOKEN",
423
+ "# automatically on deployments with a connected Blob store.",
424
+ "# BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...",
425
+ "# BLOB_ACCESS=private",
426
+ "# BLOB_KEY_PREFIX=",
427
+ "",
428
+ ].join("\n"),
429
+ docs: [
430
+ "## Vercel Blob storage",
431
+ "",
432
+ "- Package: `@beignet/provider-storage-vercel-blob`",
433
+ "- Peer dependency: `@vercel/blob`",
434
+ "- The preset wires `vercelBlobStorageProvider` in `server/providers.ts` and defers `storage` in `infra/app-ports.ts`.",
435
+ "- On Vercel, connect a Blob store to the project and `BLOB_READ_WRITE_TOKEN` is set automatically; set it manually elsewhere.",
436
+ "- The store is uniform-visibility (`BLOB_ACCESS`, default `private`): Vercel Blob does not report per-object access back, so mixed visibility needs a second store.",
437
+ "- Use `ctx.ports.storage` in app code; `ctx.ports.vercelBlob` is available as an escape hatch.",
438
+ "",
439
+ ].join("\n"),
440
+ nextSteps: [
441
+ "Run your package manager install command.",
442
+ "Connect a Vercel Blob store to the project (or set BLOB_READ_WRITE_TOKEN manually).",
443
+ "Run beignet providers audit and beignet doctor --strict.",
444
+ ],
445
+ conflictingProviderEntries: [
446
+ "localStorageProvider",
447
+ "createLocalStorageProvider",
448
+ "s3StorageProvider",
449
+ "createS3StorageProvider",
399
450
  ],
400
451
  },
401
452
  "upstash-rate-limit": {
@@ -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 {
@@ -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
 
@@ -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(
@@ -49,6 +49,7 @@ export const externalVersions = {
49
49
  drizzleOrm: "^0.45.2",
50
50
  inngest: "^3.0.0",
51
51
  libsqlClient: "^0.14.0",
52
+ vercelBlob: "^2.5.0",
52
53
  mysql2: "^3.12.0",
53
54
  pg: "^8.13.0",
54
55
  pglite: "^0.5.2",