@beignet/cli 0.0.4 → 0.0.6

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 (73) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/choices.d.ts +5 -28
  3. package/dist/choices.d.ts.map +1 -1
  4. package/dist/choices.js +0 -35
  5. package/dist/choices.js.map +1 -1
  6. package/dist/create-prompts.d.ts +5 -6
  7. package/dist/create-prompts.d.ts.map +1 -1
  8. package/dist/create-prompts.js +19 -77
  9. package/dist/create-prompts.js.map +1 -1
  10. package/dist/create.d.ts +6 -4
  11. package/dist/create.d.ts.map +1 -1
  12. package/dist/create.js +15 -24
  13. package/dist/create.js.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +32 -39
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib.d.ts +1 -1
  18. package/dist/lib.d.ts.map +1 -1
  19. package/dist/make.d.ts.map +1 -1
  20. package/dist/make.js +153 -1
  21. package/dist/make.js.map +1 -1
  22. package/dist/templates/base.d.ts +13 -0
  23. package/dist/templates/base.d.ts.map +1 -0
  24. package/dist/templates/base.js +300 -0
  25. package/dist/templates/base.js.map +1 -0
  26. package/dist/templates/db.d.ts +15 -0
  27. package/dist/templates/db.d.ts.map +1 -0
  28. package/dist/templates/db.js +1001 -0
  29. package/dist/templates/db.js.map +1 -0
  30. package/dist/templates/index.d.ts +14 -0
  31. package/dist/templates/index.d.ts.map +1 -0
  32. package/dist/templates/index.js +124 -0
  33. package/dist/templates/index.js.map +1 -0
  34. package/dist/templates/server.d.ts +26 -0
  35. package/dist/templates/server.d.ts.map +1 -0
  36. package/dist/templates/server.js +525 -0
  37. package/dist/templates/server.js.map +1 -0
  38. package/dist/templates/shadcn.d.ts +5 -0
  39. package/dist/templates/shadcn.d.ts.map +1 -0
  40. package/dist/templates/shadcn.js +555 -0
  41. package/dist/templates/shadcn.js.map +1 -0
  42. package/dist/templates/shared.d.ts +49 -0
  43. package/dist/templates/shared.d.ts.map +1 -0
  44. package/dist/templates/shared.js +57 -0
  45. package/dist/templates/shared.js.map +1 -0
  46. package/dist/templates/shell.d.ts +5 -0
  47. package/dist/templates/shell.d.ts.map +1 -0
  48. package/dist/templates/shell.js +1198 -0
  49. package/dist/templates/shell.js.map +1 -0
  50. package/dist/templates/todos.d.ts +17 -0
  51. package/dist/templates/todos.d.ts.map +1 -0
  52. package/dist/templates/todos.js +607 -0
  53. package/dist/templates/todos.js.map +1 -0
  54. package/package.json +2 -2
  55. package/src/choices.ts +4 -58
  56. package/src/create-prompts.ts +20 -88
  57. package/src/create.ts +21 -38
  58. package/src/index.ts +39 -45
  59. package/src/lib.ts +1 -1
  60. package/src/make.ts +208 -1
  61. package/src/templates/base.ts +377 -0
  62. package/src/templates/db.ts +1002 -0
  63. package/src/templates/index.ts +149 -0
  64. package/src/templates/server.ts +533 -0
  65. package/src/templates/shadcn.ts +570 -0
  66. package/src/templates/shared.ts +90 -0
  67. package/src/templates/shell.ts +1227 -0
  68. package/src/templates/todos.ts +607 -0
  69. package/dist/templates.d.ts +0 -26
  70. package/dist/templates.d.ts.map +0 -1
  71. package/dist/templates.js +0 -3995
  72. package/dist/templates.js.map +0 -1
  73. package/src/templates.ts +0 -4280
package/src/make.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import type { MakeFeatureAddon } from "./choices.js";
@@ -1106,12 +1107,18 @@ export async function makeSchedule(
1106
1107
  requireServer: Boolean(options.route),
1107
1108
  });
1108
1109
 
1110
+ // Plan the CRON_SECRET wiring before writing any files so an anchor miss
1111
+ // in lib/env.ts aborts the generator without leaving partial output.
1112
+ const cronSecretPlan = options.route
1113
+ ? await planCronSecretWiring(targetDir)
1114
+ : undefined;
1115
+
1109
1116
  const files = [
1110
1117
  ...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
1111
1118
  ...scheduleFiles(names, config, { route: Boolean(options.route) }),
1112
1119
  ];
1113
1120
 
1114
- return makeFeatureArtifact({
1121
+ const result = await makeFeatureArtifact({
1115
1122
  targetDir,
1116
1123
  config,
1117
1124
  force: Boolean(options.force),
@@ -1123,6 +1130,139 @@ export async function makeSchedule(
1123
1130
  "@beignet/core": beignetDependencyVersion,
1124
1131
  },
1125
1132
  });
1133
+
1134
+ const registryResult = await updateScheduleRegistry(
1135
+ targetDir,
1136
+ names,
1137
+ config,
1138
+ { dryRun: Boolean(options.dryRun) },
1139
+ );
1140
+ if (registryResult === "created")
1141
+ result.createdFiles.push(config.paths.schedules);
1142
+ if (registryResult === "updated")
1143
+ result.updatedFiles.push(config.paths.schedules);
1144
+ if (registryResult === "skipped")
1145
+ result.skippedFiles.push(config.paths.schedules);
1146
+ if (!result.files.includes(config.paths.schedules)) {
1147
+ result.files.push(config.paths.schedules);
1148
+ }
1149
+
1150
+ if (cronSecretPlan) {
1151
+ const applied = await applyCronSecretWiring(targetDir, cronSecretPlan, {
1152
+ dryRun: Boolean(options.dryRun),
1153
+ });
1154
+ result.createdFiles.push(...applied.createdFiles);
1155
+ result.updatedFiles.push(...applied.updatedFiles);
1156
+ result.skippedFiles.push(...applied.skippedFiles);
1157
+ }
1158
+
1159
+ return result;
1160
+ }
1161
+
1162
+ const cronSecretEnvFile = "lib/env.ts";
1163
+ const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
1164
+ // The literal APP_URL line every generated lib/env.ts ships with. CRON_SECRET
1165
+ // slots in directly below it, matching the documented env shape.
1166
+ const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1167
+
1168
+ type CronSecretWiringPlan = {
1169
+ files: Array<GeneratedFile & { result: "created" | "updated" }>;
1170
+ skippedFiles: string[];
1171
+ };
1172
+
1173
+ async function planCronSecretWiring(
1174
+ targetDir: string,
1175
+ ): Promise<CronSecretWiringPlan> {
1176
+ const plan: CronSecretWiringPlan = { files: [], skippedFiles: [] };
1177
+ const manualInstruction = `Add ${cronSecretEnvEntry.trim()} to the createEnv server block in ${cronSecretEnvFile} manually, or restore the generated env file before running make schedule --route.`;
1178
+
1179
+ const envSource = await readOptionalFile(
1180
+ path.join(targetDir, cronSecretEnvFile),
1181
+ );
1182
+ if (envSource === undefined) {
1183
+ throw new Error(
1184
+ `Could not find the generated env file ${cronSecretEnvFile}. ${manualInstruction}`,
1185
+ );
1186
+ }
1187
+
1188
+ if (/\bCRON_SECRET\b/.test(envSource)) {
1189
+ plan.skippedFiles.push(cronSecretEnvFile);
1190
+ } else if (!envSource.includes(cronSecretEnvAnchor)) {
1191
+ throw new Error(
1192
+ `Could not find the generated APP_URL server entry in ${cronSecretEnvFile}. ${manualInstruction}`,
1193
+ );
1194
+ } else {
1195
+ plan.files.push({
1196
+ path: cronSecretEnvFile,
1197
+ content: envSource.replace(
1198
+ cronSecretEnvAnchor,
1199
+ `${cronSecretEnvAnchor}\n${cronSecretEnvEntry}`,
1200
+ ),
1201
+ result: "updated",
1202
+ });
1203
+ }
1204
+
1205
+ const secretLine = `CRON_SECRET=${randomBytes(16).toString("hex")}`;
1206
+
1207
+ const envExample = await readOptionalFile(
1208
+ path.join(targetDir, ".env.example"),
1209
+ );
1210
+ if (envExample === undefined) {
1211
+ plan.files.push({
1212
+ path: ".env.example",
1213
+ content: `${secretLine}\n`,
1214
+ result: "created",
1215
+ });
1216
+ } else if (/^CRON_SECRET=/m.test(envExample)) {
1217
+ plan.skippedFiles.push(".env.example");
1218
+ } else {
1219
+ plan.files.push({
1220
+ path: ".env.example",
1221
+ content: `${envExample.replace(/\n*$/, "\n")}${secretLine}\n`,
1222
+ result: "updated",
1223
+ });
1224
+ }
1225
+
1226
+ const envLocal = await readOptionalFile(path.join(targetDir, ".env.local"));
1227
+ if (envLocal !== undefined) {
1228
+ if (/^CRON_SECRET=/m.test(envLocal)) {
1229
+ plan.skippedFiles.push(".env.local");
1230
+ } else {
1231
+ plan.files.push({
1232
+ path: ".env.local",
1233
+ content: `${envLocal.replace(/\n*$/, "\n")}${secretLine}\n`,
1234
+ result: "updated",
1235
+ });
1236
+ }
1237
+ }
1238
+
1239
+ return plan;
1240
+ }
1241
+
1242
+ async function applyCronSecretWiring(
1243
+ targetDir: string,
1244
+ plan: CronSecretWiringPlan,
1245
+ options: { dryRun: boolean },
1246
+ ): Promise<{
1247
+ createdFiles: string[];
1248
+ updatedFiles: string[];
1249
+ skippedFiles: string[];
1250
+ }> {
1251
+ if (!options.dryRun) {
1252
+ for (const file of plan.files) {
1253
+ await writeFile(path.join(targetDir, file.path), file.content);
1254
+ }
1255
+ }
1256
+
1257
+ return {
1258
+ createdFiles: plan.files
1259
+ .filter((file) => file.result === "created")
1260
+ .map((file) => file.path),
1261
+ updatedFiles: plan.files
1262
+ .filter((file) => file.result === "updated")
1263
+ .map((file) => file.path),
1264
+ skippedFiles: plan.skippedFiles,
1265
+ };
1126
1266
  }
1127
1267
 
1128
1268
  export async function makeUpload(
@@ -4725,6 +4865,73 @@ export async function stopTaskContext(): Promise<void> {
4725
4865
  return "updated";
4726
4866
  }
4727
4867
 
4868
+ async function updateScheduleRegistry(
4869
+ targetDir: string,
4870
+ names: ScheduleNames,
4871
+ config: ResolvedBeignetConfig,
4872
+ options: { dryRun: boolean },
4873
+ ): Promise<WriteGeneratedFileResult> {
4874
+ const destination = path.join(targetDir, config.paths.schedules);
4875
+ const existing = await readOptionalFile(destination);
4876
+ const featureSchedulesPath = path.join(
4877
+ featureArtifactDir(names, "schedules", config),
4878
+ "index.ts",
4879
+ );
4880
+ const registryName = `${names.featureSingularCamel}Schedules`;
4881
+ const importLine = `import { ${registryName} } from "${aliasModule(featureSchedulesPath)}";`;
4882
+ const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
4883
+ const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
4884
+ const serverImport = `import { server } from "${relativeModule(
4885
+ config.paths.schedules,
4886
+ config.paths.server,
4887
+ )}";`;
4888
+ const registryEntry = `...${registryName}`;
4889
+
4890
+ if (existing === undefined) {
4891
+ const content = `${serviceActorImport}
4892
+ ${appContextImport}
4893
+ ${importLine}
4894
+ ${serverImport}
4895
+
4896
+ export const schedules = [${registryEntry}] as const;
4897
+
4898
+ export async function createScheduleContext(): Promise<AppContext> {
4899
+ \treturn server.createServiceContext({
4900
+ \t\tactor: createServiceActor("beignet-schedule"),
4901
+ \t});
4902
+ }
4903
+
4904
+ export async function stopScheduleContext(): Promise<void> {
4905
+ \tawait server.stop();
4906
+ }
4907
+ `;
4908
+ if (!options.dryRun) {
4909
+ await mkdir(path.dirname(destination), { recursive: true });
4910
+ await writeFile(destination, content);
4911
+ }
4912
+ return "created";
4913
+ }
4914
+
4915
+ let next = existing;
4916
+ if (!next.includes(importLine)) {
4917
+ next = insertAfterImports(next, importLine);
4918
+ }
4919
+
4920
+ const registry = arrayInitializerInfo(next, "schedules");
4921
+ if (!registry) {
4922
+ next = `${next.trimEnd()}\n\nexport const schedules = [${registryEntry}] as const;\n`;
4923
+ } else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
4924
+ const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
4925
+ next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
4926
+ registry.end,
4927
+ )}`;
4928
+ }
4929
+
4930
+ if (next === existing) return "skipped";
4931
+ if (!options.dryRun) await writeFile(destination, next);
4932
+ return "updated";
4933
+ }
4934
+
4728
4935
  function arrayInitializerInfo(
4729
4936
  source: string,
4730
4937
  constName: string,
@@ -0,0 +1,377 @@
1
+ import { shadcnDependencies, shadcnDevDependencies } from "./shadcn.js";
2
+ import {
3
+ externalVersions,
4
+ hasIntegration,
5
+ integrationDeps,
6
+ json,
7
+ packageRunner,
8
+ type TemplateContext,
9
+ } from "./shared.js";
10
+ import { shellDependencies, shellDevDependencies } from "./shell.js";
11
+
12
+ export function packageJson(ctx: TemplateContext): string {
13
+ const dependencies: Record<string, string> = {
14
+ "@beignet/core": ctx.beignetVersion,
15
+ "@beignet/devtools": ctx.beignetVersion,
16
+ "@beignet/next": ctx.beignetVersion,
17
+ "@beignet/provider-auth-better-auth": ctx.beignetVersion,
18
+ "@beignet/provider-drizzle-turso": ctx.beignetVersion,
19
+ "@beignet/provider-logger-pino": ctx.beignetVersion,
20
+ "@libsql/client": externalVersions.libsqlClient,
21
+ "better-auth": externalVersions.betterAuth,
22
+ "drizzle-orm": externalVersions.drizzleOrm,
23
+ next: externalVersions.next,
24
+ pino: externalVersions.pino,
25
+ react: externalVersions.react,
26
+ "react-dom": externalVersions.reactDom,
27
+ zod: externalVersions.zod,
28
+ };
29
+
30
+ if (!ctx.api) {
31
+ dependencies["@beignet/react-query"] = ctx.beignetVersion;
32
+ dependencies["@beignet/react-hook-form"] = ctx.beignetVersion;
33
+ dependencies["@hookform/resolvers"] = externalVersions.hookformResolvers;
34
+ dependencies["@tanstack/react-query"] = externalVersions.tanstackReactQuery;
35
+ dependencies["react-hook-form"] = externalVersions.reactHookForm;
36
+
37
+ for (const [name, version] of Object.entries(shadcnDependencies)) {
38
+ dependencies[name] = version;
39
+ }
40
+ for (const [name, version] of Object.entries(shellDependencies)) {
41
+ dependencies[name] = version;
42
+ }
43
+ }
44
+
45
+ for (const integration of ctx.integrations) {
46
+ for (const [name, version] of Object.entries(
47
+ integrationDeps[integration],
48
+ )) {
49
+ dependencies[name] = version || ctx.beignetVersion;
50
+ }
51
+ }
52
+
53
+ const devDependencies: Record<string, string> = {
54
+ "@beignet/cli": ctx.beignetVersion,
55
+ "@beignet/web": ctx.beignetVersion,
56
+ "@types/bun": externalVersions.typesBun,
57
+ "@types/node": externalVersions.typesNode,
58
+ "@types/react": externalVersions.typesReact,
59
+ "@types/react-dom": externalVersions.typesReactDom,
60
+ "drizzle-kit": externalVersions.drizzleKit,
61
+ typescript: externalVersions.typescript,
62
+ };
63
+
64
+ if (!ctx.api) {
65
+ for (const [name, version] of Object.entries(shadcnDevDependencies)) {
66
+ devDependencies[name] = version;
67
+ }
68
+ for (const [name, version] of Object.entries(shellDevDependencies)) {
69
+ devDependencies[name] = version;
70
+ }
71
+ }
72
+
73
+ const scripts: Record<string, string> = {
74
+ beignet: "beignet",
75
+ dev: "next dev",
76
+ build: "next build",
77
+ start: "next start",
78
+ test: "bun test",
79
+ typecheck: "tsc --noEmit",
80
+ "db:generate": "drizzle-kit generate",
81
+ "db:migrate": "drizzle-kit migrate",
82
+ "db:reset": "bun infra/db/reset.ts",
83
+ };
84
+
85
+ return json({
86
+ name: ctx.name,
87
+ version: "0.1.0",
88
+ private: true,
89
+ type: "module",
90
+ scripts,
91
+ dependencies: sortRecord(dependencies),
92
+ devDependencies: sortRecord(devDependencies),
93
+ });
94
+ }
95
+
96
+ function sortRecord(record: Record<string, string>): Record<string, string> {
97
+ return Object.fromEntries(
98
+ Object.entries(record).sort(([left], [right]) => left.localeCompare(right)),
99
+ );
100
+ }
101
+
102
+ export function readme(ctx: TemplateContext): string {
103
+ const install = `${ctx.packageManager} install`;
104
+ const run =
105
+ ctx.packageManager === "npm"
106
+ ? "npm run dev"
107
+ : `${ctx.packageManager} run dev`;
108
+ const build =
109
+ ctx.packageManager === "npm"
110
+ ? "npm run build"
111
+ : `${ctx.packageManager} run build`;
112
+ const start =
113
+ ctx.packageManager === "npm"
114
+ ? "npm run start"
115
+ : `${ctx.packageManager} run start`;
116
+ const test =
117
+ ctx.packageManager === "npm"
118
+ ? "npm run test"
119
+ : `${ctx.packageManager} run test`;
120
+ const typecheck =
121
+ ctx.packageManager === "npm"
122
+ ? "npm run typecheck"
123
+ : `${ctx.packageManager} run typecheck`;
124
+ const cli = packageRunner(ctx);
125
+
126
+ const firstOpen = ctx.api
127
+ ? "Open http://localhost:3000 for the API landing page. The todos API under `/api/todos` requires a signed-in Better Auth user."
128
+ : "Open http://localhost:3000/sign-up, create the first account, and you land in the app shell with the todos worked example.";
129
+
130
+ const shellMap = ctx.api
131
+ ? ""
132
+ : `- \`app/(auth)/\` and \`app/(app)/\` own the sign-in/sign-up pages and the authenticated shell.
133
+ - \`components/\` owns the app shell and shadcn/ui primitives.
134
+ - \`client/\` owns the typed API client, React Query helpers, and the Better Auth client.
135
+ `;
136
+
137
+ return `# ${ctx.name}
138
+
139
+ Beignet app scaffolded with \`@beignet/cli\`.
140
+
141
+ ## Getting started
142
+
143
+ \`\`\`bash
144
+ ${install}
145
+ cp .env.example .env.local
146
+ \`\`\`
147
+
148
+ ## Prepare the database
149
+
150
+ \`\`\`bash
151
+ ${cli} db migrate
152
+ \`\`\`
153
+
154
+ The starter ships its initial Drizzle migration in \`drizzle/\`, so the first run only applies it. After you change \`infra/db/schema/\`, run \`${cli} db generate\` and \`${cli} db migrate\` together.
155
+
156
+ ## Start the app
157
+
158
+ \`\`\`bash
159
+ ${run}
160
+ \`\`\`
161
+
162
+ ${firstOpen}
163
+
164
+ ## First checks
165
+
166
+ \`\`\`bash
167
+ # in another terminal
168
+ ${cli} routes
169
+ ${cli} lint
170
+ ${cli} doctor
171
+ ${test}
172
+ ${typecheck}
173
+ \`\`\`
174
+
175
+ \`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
176
+
177
+ ## Build for production
178
+
179
+ \`\`\`bash
180
+ ${build}
181
+ ${start}
182
+ \`\`\`
183
+
184
+ ## Generate a feature
185
+
186
+ \`\`\`bash
187
+ ${cli} make feature projects
188
+ ${cli} db generate
189
+ ${cli} db migrate
190
+ ${test}
191
+ ${typecheck}
192
+ ${cli} lint
193
+ ${cli} doctor
194
+ \`\`\`
195
+
196
+ \`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.
197
+
198
+ ## App map
199
+
200
+ - \`features/todos/contracts.ts\` owns the HTTP contract and reuses shared feature schemas.
201
+ - \`features/todos/schemas.ts\` owns shared DTO and validation schemas used by contracts, use cases, ports, tests, and clients.
202
+ - \`features/todos/use-cases/\` owns application behavior and validation. Every todos use case requires a signed-in user.
203
+ - \`features/todos/policy.ts\` owns the ownership rules registered with \`createGate(...)\`.
204
+ - \`features/shared/errors.ts\` keeps application errors and route-owned error schemas together.
205
+ - \`ports/\` defines app-owned dependencies.
206
+ - \`infra/\` implements ports for the selected runtime.
207
+ - \`infra/db/schema/\` contains the Drizzle schema, \`drizzle/\` contains the checked-in migrations, and \`infra/todos/\` contains the durable todo repository adapter.
208
+ - \`server/routes.ts\` keeps the central route registry and OpenAPI contract list.
209
+ - \`server/context.ts\` declares the context blueprint shared by the server and route tests.
210
+ - \`server/providers.ts\` wires devtools, Better Auth, pino, Drizzle/libSQL, and the starter database provider.
211
+ - \`lib/env.ts\` validates deployment configuration at startup.
212
+ - \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
213
+ - \`lib/better-auth.ts\` owns Better Auth setup and keeps provider-specific auth details outside use cases.
214
+ ${shellMap}
215
+ ## Before deploying
216
+
217
+ - Create a Turso database or keep \`TURSO_DB_URL=file:local.db\` for local libSQL development.
218
+ - Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.
219
+ - Run \`${cli} db reset\` to rebuild a local SQLite database from the checked-in migrations.
220
+ - Remove \`DEVTOOLS_ENABLED=true\` in production unless you add authentication and stricter redaction.
221
+ - Set \`APP_URL\`, \`BETTER_AUTH_SECRET\`, \`LOG_LEVEL\`, and service-specific integration variables in your hosting environment.
222
+ - Set \`BETTER_AUTH_TRUSTED_ORIGINS\` before serving auth across multiple origins.
223
+ - Review the starter authorization policy before exposing user-owned data.
224
+ ${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}`;
225
+ }
226
+
227
+ export function integrationsDoc(ctx: TemplateContext): string {
228
+ const lines = [
229
+ "# Integrations",
230
+ "",
231
+ "The starter app added dependencies for the integrations you selected. Runtime providers are wired in `server/providers.ts` when Beignet can do so safely; integrations that need app-owned setup list their follow-up steps below.",
232
+ "",
233
+ ];
234
+
235
+ for (const integration of ctx.integrations) {
236
+ if (integration === "inngest") {
237
+ lines.push(
238
+ "## Inngest",
239
+ "",
240
+ "- Package: `@beignet/core` (`@beignet/core/events`, `@beignet/core/jobs`)",
241
+ "- Package: `@beignet/provider-inngest`",
242
+ "- Peer dependency: `inngest`",
243
+ "- The starter wires `inngestProvider` in `server/providers.ts`.",
244
+ "- Define jobs with `defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.",
245
+ "- Keep direct Inngest client usage inside infrastructure code unless you intentionally add an app-owned escape-hatch port.",
246
+ "",
247
+ );
248
+ }
249
+
250
+ if (integration === "resend") {
251
+ lines.push(
252
+ "## Resend mail",
253
+ "",
254
+ "- Package: `@beignet/provider-mail-resend`",
255
+ "- Peer dependency: `resend`",
256
+ "- The starter wires `mailResendProvider` in `server/providers.ts`.",
257
+ "- `.env.example` ships dev placeholders so the app boots; set real `RESEND_API_KEY` and `RESEND_FROM` values before sending mail.",
258
+ "",
259
+ );
260
+ }
261
+
262
+ if (integration === "upstash-rate-limit") {
263
+ lines.push(
264
+ "## Upstash rate limit",
265
+ "",
266
+ "- Package: `@beignet/provider-rate-limit-upstash`",
267
+ "- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
268
+ "- The starter wires `upstashRateLimitProvider` in `server/providers.ts` and adds `rateLimit: RateLimitPort` to `AppPorts`.",
269
+ "- `.env.example` ships dev placeholders so the app boots; set real `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` values before rate limiting requests.",
270
+ "",
271
+ );
272
+ }
273
+ }
274
+
275
+ return `${lines.join("\n")}\n`;
276
+ }
277
+
278
+ export function envExample(ctx: TemplateContext): string {
279
+ const lines = [
280
+ "# Copy to .env.local and fill in values for your app.",
281
+ "APP_URL=http://localhost:3000",
282
+ "",
283
+ "# Dev-only default; remove in production.",
284
+ "DEVTOOLS_ENABLED=true",
285
+ "",
286
+ "LOG_LEVEL=info",
287
+ "LOG_FORMAT=json",
288
+ `LOG_SERVICE=${ctx.name}`,
289
+ "",
290
+ "# Use file:local.db for local libSQL or libsql://... for Turso cloud.",
291
+ "TURSO_DB_URL=file:local.db",
292
+ "# TURSO_DB_AUTH_TOKEN=",
293
+ "",
294
+ "BETTER_AUTH_SECRET=local-dev-better-auth-secret-change-me",
295
+ "BETTER_AUTH_URL=http://localhost:3000",
296
+ "# BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000",
297
+ "",
298
+ ];
299
+
300
+ if (hasIntegration(ctx, "inngest")) {
301
+ lines.push("INNGEST_APP_NAME=beignet-app", "# INNGEST_EVENT_KEY=", "");
302
+ }
303
+
304
+ if (hasIntegration(ctx, "resend")) {
305
+ lines.push(
306
+ "# Dev placeholders keep the app bootable; set real Resend values before sending mail.",
307
+ "RESEND_API_KEY=dev-placeholder-change-me",
308
+ "RESEND_FROM=Beignet Starter <noreply@example.com>",
309
+ "",
310
+ );
311
+ }
312
+
313
+ if (hasIntegration(ctx, "upstash-rate-limit")) {
314
+ lines.push(
315
+ "# Dev placeholders keep the app bootable; set real Upstash values before rate limiting.",
316
+ "UPSTASH_REDIS_REST_URL=https://dev-placeholder.upstash.io",
317
+ "UPSTASH_REDIS_REST_TOKEN=dev-placeholder-change-me",
318
+ "UPSTASH_PREFIX=beignet:ratelimit",
319
+ "",
320
+ );
321
+ }
322
+
323
+ return `${lines.join("\n")}\n`;
324
+ }
325
+
326
+ const files = {
327
+ gitignore: `.next
328
+ node_modules
329
+ dist
330
+ coverage
331
+ .env
332
+ .env.local
333
+ local.db
334
+ local.db-*
335
+ `,
336
+ nextEnv: `/// <reference types="next" />
337
+ /// <reference types="next/image-types/global" />
338
+
339
+ // This file should not be edited.
340
+ `,
341
+ nextConfig: `/** @type {import("next").NextConfig} */
342
+ const nextConfig = {};
343
+
344
+ export default nextConfig;
345
+ `,
346
+ tsconfig: json({
347
+ compilerOptions: {
348
+ target: "ES2017",
349
+ lib: ["dom", "dom.iterable", "esnext"],
350
+ allowJs: true,
351
+ skipLibCheck: true,
352
+ strict: true,
353
+ noEmit: true,
354
+ esModuleInterop: true,
355
+ module: "esnext",
356
+ moduleResolution: "bundler",
357
+ resolveJsonModule: true,
358
+ isolatedModules: true,
359
+ jsx: "react-jsx",
360
+ incremental: true,
361
+ plugins: [{ name: "next" }],
362
+ paths: {
363
+ "@/*": ["./*"],
364
+ },
365
+ },
366
+ include: [
367
+ "next-env.d.ts",
368
+ "**/*.ts",
369
+ "**/*.tsx",
370
+ ".next/types/**/*.ts",
371
+ ".next/dev/types/**/*.ts",
372
+ ],
373
+ exclude: ["node_modules"],
374
+ }),
375
+ };
376
+
377
+ export { files as baseFiles };