@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.
- package/CHANGELOG.md +26 -0
- package/README.md +59 -15
- package/dist/check.d.ts +61 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +164 -0
- package/dist/check.js.map +1 -0
- package/dist/choices.d.ts +8 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +14 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +8 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -4
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -21
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +121 -3
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts +63 -0
- package/dist/make/inbox.d.ts.map +1 -0
- package/dist/make/inbox.js +1747 -0
- package/dist/make/inbox.js.map +1 -0
- package/dist/make/payments.d.ts +61 -0
- package/dist/make/payments.d.ts.map +1 -0
- package/dist/make/payments.js +1900 -0
- package/dist/make/payments.js.map +1 -0
- package/dist/make/shared.d.ts +592 -0
- package/dist/make/shared.d.ts.map +1 -0
- package/dist/make/shared.js +2233 -0
- package/dist/make/shared.js.map +1 -0
- package/dist/make/tenancy.d.ts +101 -0
- package/dist/make/tenancy.d.ts.map +1 -0
- package/dist/make/tenancy.js +4409 -0
- package/dist/make/tenancy.js.map +1 -0
- package/dist/make.d.ts +8 -44
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +57 -3689
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -4
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts +19 -0
- package/dist/provider-add.d.ts.map +1 -0
- package/dist/provider-add.js +691 -0
- package/dist/provider-add.js.map +1 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +77 -1
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +9 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +32 -0
- package/dist/registry-edits.js.map +1 -1
- package/dist/task.d.ts +2 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -0
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -11
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +42 -12
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +7 -1
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +44 -37
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +5 -0
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +1 -1
- package/src/check.ts +246 -0
- package/src/choices.ts +28 -0
- package/src/db.ts +4 -4
- package/src/index.ts +232 -21
- package/src/inspect.ts +168 -2
- package/src/make/inbox.ts +2015 -0
- package/src/make/payments.ts +2182 -0
- package/src/make/shared.ts +3638 -0
- package/src/make/tenancy.ts +4809 -0
- package/src/make.ts +388 -5355
- package/src/mcp.ts +66 -3
- package/src/provider-add.ts +926 -0
- package/src/provider-audit.ts +95 -3
- package/src/registry-edits.ts +47 -0
- package/src/task.ts +8 -10
- package/src/templates/agents.ts +29 -11
- package/src/templates/base.ts +43 -12
- package/src/templates/db/sqlite.ts +7 -1
- package/src/templates/server.ts +44 -37
- package/src/templates/shared.ts +5 -0
- package/src/test-helpers/generated-app.ts +168 -0
package/src/provider-audit.ts
CHANGED
|
@@ -365,9 +365,101 @@ export async function readProviderListEntries(
|
|
|
365
365
|
)
|
|
366
366
|
).join("\n");
|
|
367
367
|
const providerListSource = extractProviderListSource(providerSource);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 {
|
package/src/registry-edits.ts
CHANGED
|
@@ -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:
|
|
39
|
-
stopTaskContext?: (ctx: unknown, args:
|
|
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
|
};
|
package/src/templates/agents.ts
CHANGED
|
@@ -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,
|
|
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
|
-
${
|
|
77
|
-
${cli} lint
|
|
78
|
-
${cli} doctor --strict
|
|
79
|
-
${test}
|
|
80
|
-
${typecheck}
|
|
93
|
+
${cli} check
|
|
81
94
|
\`\`\`
|
|
82
95
|
|
|
83
|
-
\`${lint
|
|
84
|
-
\`${cli}
|
|
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
|
}
|
package/src/templates/base.ts
CHANGED
|
@@ -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
|
-
${
|
|
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.
|
|
252
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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(
|
package/src/templates/server.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
export const HEAD =
|
|
564
|
-
|
|
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 {
|
|
586
|
+
readyRoute: `import { createHealthRoute } from "@beignet/next";
|
|
588
587
|
import { env } from "@/lib/env";
|
|
589
|
-
import {
|
|
588
|
+
import { getServer } from "@/server";
|
|
590
589
|
|
|
591
|
-
const
|
|
592
|
-
|
|
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 {
|
|
603
|
+
import { getServer } from "@/server";
|
|
617
604
|
|
|
618
|
-
export const { GET, POST } = createDevtoolsRoute(
|
|
619
|
-
|
|
620
|
-
|
|
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 {
|
|
615
|
+
import { contracts } from "@/server/routes";
|
|
626
616
|
|
|
627
|
-
export const GET = createOpenAPIHandler(
|
|
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
|
-
|
|
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
|
package/src/templates/shared.ts
CHANGED
|
@@ -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
|
};
|