@beignet/cli 0.0.27 → 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 (49) hide show
  1. package/CHANGELOG.md +12 -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/db.d.ts +8 -0
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +2 -2
  10. package/dist/db.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +47 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspect.d.ts.map +1 -1
  15. package/dist/inspect.js +70 -0
  16. package/dist/inspect.js.map +1 -1
  17. package/dist/make/shared.d.ts +17 -0
  18. package/dist/make/shared.d.ts.map +1 -1
  19. package/dist/make/shared.js +67 -0
  20. package/dist/make/shared.js.map +1 -1
  21. package/dist/make.d.ts.map +1 -1
  22. package/dist/make.js +11 -55
  23. package/dist/make.js.map +1 -1
  24. package/dist/mcp.js +1 -1
  25. package/dist/mcp.js.map +1 -1
  26. package/dist/provider-audit.d.ts.map +1 -1
  27. package/dist/provider-audit.js +77 -1
  28. package/dist/provider-audit.js.map +1 -1
  29. package/dist/templates/agents.d.ts.map +1 -1
  30. package/dist/templates/agents.js +27 -9
  31. package/dist/templates/agents.js.map +1 -1
  32. package/dist/templates/base.d.ts.map +1 -1
  33. package/dist/templates/base.js +42 -12
  34. package/dist/templates/base.js.map +1 -1
  35. package/dist/templates/db/sqlite.d.ts.map +1 -1
  36. package/dist/templates/db/sqlite.js +7 -1
  37. package/dist/templates/db/sqlite.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/check.ts +246 -0
  40. package/src/db.ts +2 -2
  41. package/src/index.ts +62 -8
  42. package/src/inspect.ts +100 -0
  43. package/src/make/shared.ts +88 -0
  44. package/src/make.ts +10 -68
  45. package/src/mcp.ts +1 -1
  46. package/src/provider-audit.ts +95 -3
  47. package/src/templates/agents.ts +27 -9
  48. package/src/templates/base.ts +43 -12
  49. package/src/templates/db/sqlite.ts +7 -1
@@ -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(