@flaredev/cli 0.4.0 → 0.5.0

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 (59) hide show
  1. package/dist/index.js +851 -405
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/actions.ts +38 -5
  4. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  5. package/templates/app/app/dashboard/page.tsx +4 -7
  6. package/templates/app/app/globals.css +0 -6
  7. package/templates/app/app/layout.tsx +37 -28
  8. package/templates/app/app/page.tsx +10 -0
  9. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  10. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  11. package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
  12. package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
  13. package/templates/app/components/dashboard/import-dialog.tsx +2 -2
  14. package/templates/app/components/dashboard/record-actions.tsx +9 -9
  15. package/templates/app/components/dashboard/record-detail.tsx +6 -3
  16. package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
  17. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  18. package/templates/app/components/dashboard/resource-form.tsx +261 -165
  19. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  20. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  21. package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
  22. package/templates/app/components/dashboard/resource-table.tsx +12 -9
  23. package/templates/app/components/dashboard/row-actions.tsx +5 -5
  24. package/templates/app/lib/costs.ts +138 -0
  25. package/templates/app/lib/dashboard-nav.ts +5 -1
  26. package/templates/app/lib/dashboard.ts +66 -61
  27. package/templates/app/lib/number.ts +33 -0
  28. package/templates/app/lib/site.ts +56 -56
  29. package/templates/app/lib/suggest.ts +35 -0
  30. package/templates/app/lib/usage.ts +32 -0
  31. package/templates/locks/package-lock.json +9 -9
  32. package/templates/locks/pnpm-lock.yaml +12 -12
  33. package/templates/locks/yarn.lock +3 -3
  34. package/templates/next/.env.example +21 -0
  35. package/templates/next/README.md +16 -0
  36. package/templates/next/app/api/health/route.ts +7 -0
  37. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  38. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  39. package/templates/next/app/dashboard/actions.ts +277 -0
  40. package/templates/next/app/dashboard/layout.tsx +64 -0
  41. package/templates/next/app/dashboard/page.tsx +127 -0
  42. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  43. package/templates/next/lib/analytics.ts +71 -0
  44. package/templates/next/lib/audit.ts +80 -0
  45. package/templates/next/lib/auth-emails.ts +92 -0
  46. package/templates/next/lib/auth.ts +156 -0
  47. package/templates/next/lib/dashboard.ts +161 -0
  48. package/templates/next/lib/db.ts +24 -0
  49. package/templates/next/lib/mail.ts +35 -0
  50. package/templates/next/lib/realtime.ts +23 -0
  51. package/templates/next/lib/redis.ts +17 -0
  52. package/templates/next/lib/storage.ts +57 -0
  53. package/templates/next/lib/theme.ts +83 -0
  54. package/templates/next/lib/views.ts +23 -0
  55. package/templates/next/next.config.ts +13 -0
  56. package/templates/next/prisma/schema/base.prisma +162 -0
  57. package/templates/next/prisma.config.ts +15 -0
  58. package/templates/next/tsconfig.json +24 -0
  59. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
- import pc23 from "picocolors";
2
+ import pc24 from "picocolors";
3
3
 
4
4
  // src/cli.ts
5
5
  import { cac } from "cac";
6
- import pc22 from "picocolors";
6
+ import pc23 from "picocolors";
7
7
  import * as prompts3 from "@clack/prompts";
8
8
  import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
9
9
 
10
10
  // src/commands/create.ts
11
11
  import { randomBytes } from "crypto";
12
- import { appendFileSync, copyFileSync, existsSync as existsSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
13
- import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
12
+ import { appendFileSync, copyFileSync, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
13
+ import { basename, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
14
14
  import * as prompts from "@clack/prompts";
15
15
  import pc from "picocolors";
16
16
  import { FLARE_VERSION } from "@flaredev/core";
@@ -139,6 +139,7 @@ function parseTheme(value) {
139
139
  }
140
140
 
141
141
  // src/versions.ts
142
+ var COMPATIBILITY_DATE = "2026-09-15";
142
143
  var APP_DEPENDENCIES = {
143
144
  "@better-auth/drizzle-adapter": "1.7.5",
144
145
  "@better-auth/passkey": "1.7.5",
@@ -185,42 +186,90 @@ var APP_DEV_DEPENDENCIES = {
185
186
  vite: "^8.3.0",
186
187
  wrangler: "^4.132.0"
187
188
  };
189
+ var NEXT_DEPENDENCIES = {
190
+ next: "16.0.4",
191
+ "@prisma/client": "7.10.0",
192
+ "@prisma/adapter-pg": "7.10.0",
193
+ pg: "^8.16.3",
194
+ "@upstash/redis": "^1.36.0",
195
+ "@better-auth/prisma-adapter": "1.7.5",
196
+ // Signs S3 requests in a few kilobytes, where the AWS SDK is megabytes.
197
+ aws4fetch: "^1.0.20",
198
+ dotenv: "^17.2.3"
199
+ };
200
+ var NEXT_DEV_DEPENDENCIES = {
201
+ prisma: "7.10.0",
202
+ // Runs seeds: the generated Prisma client is TypeScript a bundler compiles.
203
+ tsx: "^4.23.13",
204
+ "@types/node": "^22.20.3",
205
+ "@types/react": "^19.3.0",
206
+ "@types/react-dom": "^19.3.0",
207
+ "@tailwindcss/postcss": "^4.3.3",
208
+ tailwindcss: "^4.3.3",
209
+ typescript: "~5.9.3"
210
+ };
211
+ var CLOUDFLARE_ONLY = [
212
+ "wrangler.jsonc",
213
+ "vite.config.ts",
214
+ "worker",
215
+ "db",
216
+ "migrations",
217
+ "drizzle.config.ts",
218
+ ".dev.vars.example",
219
+ "worker-configuration.d.ts"
220
+ ];
221
+
222
+ // src/stack.ts
223
+ import { existsSync, readFileSync, writeFileSync } from "fs";
224
+ import { join } from "path";
225
+ var STACKS = ["cloudflare", "next"];
226
+ var isStack = (value) => STACKS.includes(value);
227
+ function readStack(appRoot) {
228
+ const file = join(appRoot, "package.json");
229
+ if (!existsSync(file)) return "cloudflare";
230
+ try {
231
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
232
+ return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
233
+ } catch {
234
+ return "cloudflare";
235
+ }
236
+ }
188
237
 
189
238
  // src/utils/fs.ts
190
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
191
- import { dirname, join, resolve } from "path";
239
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
240
+ import { dirname, join as join2, resolve } from "path";
192
241
  import { fileURLToPath } from "url";
193
242
  var RENAMES = { _gitignore: ".gitignore" };
194
243
  function copyTemplate(from, to, tokens) {
195
244
  mkdirSync(to, { recursive: true });
196
245
  let files = 0;
197
246
  for (const entry of readdirSync(from)) {
198
- const src = join(from, entry);
199
- const dest = join(to, RENAMES[entry] ?? entry);
247
+ const src = join2(from, entry);
248
+ const dest = join2(to, RENAMES[entry] ?? entry);
200
249
  if (statSync(src).isDirectory()) {
201
250
  files += copyTemplate(src, dest, tokens);
202
251
  continue;
203
252
  }
204
- const content = readFileSync(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
205
- writeFileSync(dest, content);
253
+ const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
254
+ writeFileSync2(dest, content);
206
255
  files++;
207
256
  }
208
257
  return files;
209
258
  }
210
259
  function writeJson(path, value) {
211
260
  mkdirSync(dirname(path), { recursive: true });
212
- writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
261
+ writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
213
262
  }
214
263
  function findUp(file, start) {
215
264
  let dir = resolve(start);
216
265
  while (true) {
217
- if (existsSync(join(dir, file))) return dir;
266
+ if (existsSync2(join2(dir, file))) return dir;
218
267
  const parent = dirname(dir);
219
268
  if (parent === dir) return void 0;
220
269
  dir = parent;
221
270
  }
222
271
  }
223
- var templatesDir = join(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
272
+ var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
224
273
 
225
274
  // src/utils/pm.ts
226
275
  import { spawnSync } from "child_process";
@@ -259,7 +308,7 @@ function toAppName(dir) {
259
308
  function flarePackageSpec(pkg, appDir, packageManager) {
260
309
  const repoPackage = resolve2(templatesDir, "../..", pkg);
261
310
  const repoRoot = resolve2(repoPackage, "../..");
262
- const fromCheckout = existsSync2(join2(repoPackage, "package.json")) && existsSync2(join2(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
311
+ const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
263
312
  if (!fromCheckout) return `^${FLARE_VERSION}`;
264
313
  const rel = relative(repoRoot, appDir);
265
314
  if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
@@ -269,32 +318,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
269
318
  function createApp(target, options2 = {}) {
270
319
  const dir = resolve2(target);
271
320
  const name = toAppName(dir);
272
- if (existsSync2(dir) && readdirSync2(dir).length > 0) {
321
+ if (existsSync3(dir) && readdirSync2(dir).length > 0) {
273
322
  throw new Error(`Directory ${dir} already exists and is not empty.`);
274
323
  }
275
324
  if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
276
325
  throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
277
326
  }
278
327
  const packageManager = options2.pm ?? detectPackageManager();
328
+ if (options2.stack !== void 0 && !isStack(options2.stack)) {
329
+ throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
330
+ }
331
+ const stack = options2.stack ?? "cloudflare";
279
332
  const authProviders = parseAuthProviders(options2.authProviders);
280
333
  const authMethods = parseAuthMethods(options2.auth);
281
334
  const theme = parseTheme(options2.theme);
282
335
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
283
- const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
284
- const files = copyTemplate(join2(templatesDir, "app"), dir, {
336
+ const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
337
+ const files = copyTemplate(join3(templatesDir, "app"), dir, {
285
338
  APP_NAME: name,
286
339
  COMPAT_DATE: compatibilityDate,
287
340
  PM: packageManager,
288
341
  THEME: theme
289
342
  });
290
- writeFileSync2(join2(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
291
- appendFileSync(join2(dir, ".dev.vars.example"), devVarsExampleEntries(authProviders));
292
- writeJson(join2(dir, "package.json"), {
343
+ if (stack === "next") {
344
+ copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
345
+ for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
346
+ }
347
+ writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
348
+ appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
349
+ const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
350
+ const uiOnly = Object.fromEntries(
351
+ Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
352
+ );
353
+ writeJson(join3(dir, "package.json"), {
293
354
  name,
294
355
  version: "0.1.0",
295
356
  private: true,
296
357
  type: "module",
297
- scripts: {
358
+ // Which stack this app targets. `flare gen` reads it; don't change it by hand.
359
+ flare: { stack },
360
+ scripts: stack === "next" ? {
361
+ dev: "next dev",
362
+ build: "prisma generate && next build",
363
+ start: "next start",
364
+ deploy: "vercel deploy --prod",
365
+ "db:generate": "prisma generate",
366
+ "db:migrate": "prisma migrate dev",
367
+ "db:studio": "prisma studio"
368
+ } : {
298
369
  dev: "flare dev",
299
370
  build: "flare build",
300
371
  start: "flare start",
@@ -303,18 +374,25 @@ function createApp(target, options2 = {}) {
303
374
  "db:generate": "drizzle-kit generate",
304
375
  "db:migrate:local": "wrangler d1 migrations apply DB --local"
305
376
  },
306
- dependencies: { "@flaredev/core": flarePackageSpec("core", dir, packageManager), ...APP_DEPENDENCIES },
307
- devDependencies: { "@flaredev/cli": flarePackageSpec("cli", dir, packageManager), ...APP_DEV_DEPENDENCIES }
377
+ dependencies: {
378
+ "@flaredev/core": flarePackageSpec("core", dir, packageManager),
379
+ ...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
380
+ },
381
+ devDependencies: {
382
+ "@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
383
+ ...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
384
+ }
308
385
  });
309
- writeFileSync2(
310
- join2(dir, ".dev.vars"),
386
+ writeFileSync3(
387
+ join3(dir, stack === "next" ? ".env" : ".dev.vars"),
311
388
  `BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
312
389
  RESEND_API_KEY=
313
390
  MAIL_FROM=
314
391
  ${devVarsEntries(authProviders)}`
315
392
  );
316
393
  if (packageManager === "pnpm" && !inWorkspace) {
317
- writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
394
+ const allowBuilds = stack === "next" ? "allowBuilds:\n '@prisma/engines': true\n prisma: true\n sharp: false\n" : "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n";
395
+ writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
318
396
  }
319
397
  const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
320
398
  if (pinned) copyLockfile(packageManager, dir);
@@ -325,9 +403,9 @@ ${devVarsEntries(authProviders)}`
325
403
  function copyLockfile(packageManager, dir) {
326
404
  const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
327
405
  if (!file) return false;
328
- const source = join2(templatesDir, "locks", file);
329
- if (!existsSync2(source)) return false;
330
- copyFileSync(source, join2(dir, file));
406
+ const source = join3(templatesDir, "locks", file);
407
+ if (!existsSync3(source)) return false;
408
+ copyFileSync(source, join3(dir, file));
331
409
  return true;
332
410
  }
333
411
  function printNextSteps(result, installed) {
@@ -347,8 +425,8 @@ function printNextSteps(result, installed) {
347
425
  }
348
426
 
349
427
  // src/commands/gen-billing.ts
350
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
351
- import { dirname as dirname6, join as join11 } from "path";
428
+ import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
429
+ import { dirname as dirname6, join as join12 } from "path";
352
430
  import pc6 from "picocolors";
353
431
 
354
432
  // src/generator/grammar.ts
@@ -379,6 +457,20 @@ function splitTopLevel(input) {
379
457
  parts.push(current);
380
458
  return parts.map((part) => part.trim()).filter(Boolean);
381
459
  }
460
+ var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
461
+ var SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
462
+ function parseSize(text, spec) {
463
+ const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i.exec(text.trim());
464
+ if (!match) {
465
+ throw new FieldGrammarError(`"${spec}": "${text}" isn't a size. Write it like "file:[image]:5mb".`);
466
+ }
467
+ const bytes = Math.round(Number(match[1]) * SIZE_UNITS[match[2].toLowerCase()]);
468
+ if (bytes < 1) throw new FieldGrammarError(`"${spec}": a size limit has to be at least one byte.`);
469
+ if (bytes > MAX_UPLOAD_BYTES) {
470
+ throw new FieldGrammarError(`"${spec}": ${text} is more than a Worker will carry in one request; the most is 100mb.`);
471
+ }
472
+ return bytes;
473
+ }
382
474
  function editDistance(a, b) {
383
475
  const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
384
476
  for (let j = 1; j <= b.length; j++) dp[0][j] = j;
@@ -439,11 +531,14 @@ function parseField(spec) {
439
531
  }
440
532
  if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
441
533
  field.options = options2;
442
- } else if (match = /^file(?::\[(.*)\])?$/.exec(type)) {
534
+ } else if (/^file\[/.test(type)) {
535
+ throw new FieldGrammarError(`"${spec}": file fields need a colon before the list, e.g. "${field.key}:file:[${type.slice(5, -1) || "image"}]".`);
536
+ } else if (match = /^file(?::\[(.*)\])?(?::([^:]*))?$/.exec(type)) {
443
537
  field.kind = "file";
444
538
  if (match[1] === void 0) {
445
539
  throw new FieldGrammarError(`"${spec}": file fields list accepted types, e.g. "file:[image,pdf]".`);
446
540
  }
541
+ if (match[2] !== void 0) field.maxBytes = parseSize(match[2], spec);
447
542
  const categories = Object.keys(FILE_CATEGORIES);
448
543
  const accept = list(match[1], "file type", spec).map((category) => category.toLowerCase());
449
544
  for (const category of accept) {
@@ -495,7 +590,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
495
590
 
496
591
  // src/generator/markers.ts
497
592
  import { createHash } from "crypto";
498
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
593
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
499
594
  import { dirname as dirname3 } from "path";
500
595
  var START = "// generated:start";
501
596
  var END = "// generated:end";
@@ -545,8 +640,8 @@ ${indentBlock(body, indent)}${indent}${END}
545
640
  ${after}`;
546
641
  }
547
642
  function inspectGenerated(path, content) {
548
- if (!existsSync3(path)) return "missing";
549
- const parts = splitMarkers(readFileSync2(path, "utf8"));
643
+ if (!existsSync4(path)) return "missing";
644
+ const parts = splitMarkers(readFileSync3(path, "utf8"));
550
645
  if (!parts) return "unmarked";
551
646
  if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
552
647
  if (normalize(parts.block) !== normalize(content)) return "outdated";
@@ -562,18 +657,18 @@ var DriftError = class extends Error {
562
657
  };
563
658
  function writeGenerated(path, content, options2 = {}) {
564
659
  const hash = hashBlock(content);
565
- if (!existsSync3(path)) {
660
+ if (!existsSync4(path)) {
566
661
  mkdirSync2(dirname3(path), { recursive: true });
567
- writeFileSync3(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
662
+ writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
568
663
  return "create";
569
664
  }
570
- const source = readFileSync2(path, "utf8");
665
+ const source = readFileSync3(path, "utf8");
571
666
  const parts = splitMarkers(source);
572
667
  if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
573
668
  if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
574
669
  const next = joinMarkers({ ...parts, block: content, hash });
575
670
  if (next === source) return "identical";
576
- writeFileSync3(path, next);
671
+ writeFileSync4(path, next);
577
672
  return "update";
578
673
  }
579
674
 
@@ -592,6 +687,7 @@ function options(field) {
592
687
  if (field.format) opts.format = field.format;
593
688
  if (field.widget) opts.widget = field.widget;
594
689
  if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
690
+ if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
595
691
  return opts;
596
692
  }
597
693
  function toField(field) {
@@ -1327,17 +1423,17 @@ var billingDevVarsExampleEntries = () => [
1327
1423
  ].join("\n");
1328
1424
 
1329
1425
  // src/generator/load.ts
1330
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
1331
- import { join as join3, resolve as resolve3 } from "path";
1426
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
1427
+ import { join as join4, resolve as resolve3 } from "path";
1332
1428
  import { createJiti } from "jiti";
1333
1429
  async function loadResources(root) {
1334
1430
  const appRoot = resolve3(root);
1335
- const dir = join3(appRoot, "resources");
1336
- if (!existsSync4(dir)) return [];
1337
- const jiti = createJiti(join3(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1431
+ const dir = join4(appRoot, "resources");
1432
+ if (!existsSync5(dir)) return [];
1433
+ const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
1338
1434
  const loaded = [];
1339
1435
  for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
1340
- const path = join3(dir, file);
1436
+ const path = join4(dir, file);
1341
1437
  const mod = await jiti.import(path);
1342
1438
  const resource = mod.default;
1343
1439
  if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
@@ -1355,20 +1451,20 @@ async function loadResources(root) {
1355
1451
 
1356
1452
  // src/generator/migrations.ts
1357
1453
  import { spawn as spawn4 } from "child_process";
1358
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1359
- import { join as join8 } from "path";
1454
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1455
+ import { join as join9 } from "path";
1360
1456
 
1361
1457
  // src/commands/run.ts
1362
1458
  import { spawn as spawn3 } from "child_process";
1363
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1364
- import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
1459
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1460
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
1365
1461
  import pc4 from "picocolors";
1366
1462
 
1367
1463
  // src/tunnel.ts
1368
1464
  import { spawn, spawnSync as spawnSync2 } from "child_process";
1369
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
1465
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1370
1466
  import { homedir } from "os";
1371
- import { join as join4 } from "path";
1467
+ import { join as join5 } from "path";
1372
1468
  import pc2 from "picocolors";
1373
1469
  var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
1374
1470
  var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
@@ -1393,7 +1489,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
1393
1489
  throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
1394
1490
  }
1395
1491
  var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
1396
- var cacheDir = () => join4(homedir(), ".flare", "bin");
1492
+ var cacheDir = () => join5(homedir(), ".flare", "bin");
1397
1493
  function onPath() {
1398
1494
  const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1399
1495
  return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
@@ -1402,19 +1498,19 @@ async function download(log2) {
1402
1498
  const asset = releaseAsset();
1403
1499
  const dir = cacheDir();
1404
1500
  mkdirSync3(dir, { recursive: true });
1405
- const target = join4(dir, binName);
1501
+ const target = join5(dir, binName);
1406
1502
  log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1407
1503
  const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1408
1504
  if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1409
1505
  const bytes = Buffer.from(await response.arrayBuffer());
1410
1506
  const partial = `${target}.download`;
1411
1507
  if (asset.endsWith(".tgz")) {
1412
- writeFileSync4(`${partial}.tgz`, bytes);
1508
+ writeFileSync5(`${partial}.tgz`, bytes);
1413
1509
  const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
1414
- rmSync(`${partial}.tgz`, { force: true });
1510
+ rmSync2(`${partial}.tgz`, { force: true });
1415
1511
  if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
1416
1512
  } else {
1417
- writeFileSync4(partial, bytes);
1513
+ writeFileSync5(partial, bytes);
1418
1514
  renameSync(partial, target);
1419
1515
  }
1420
1516
  if (process.platform !== "win32") chmodSync(target, 493);
@@ -1423,8 +1519,8 @@ async function download(log2) {
1423
1519
  async function ensureCloudflared(log2 = console.log) {
1424
1520
  const found = onPath();
1425
1521
  if (found) return found;
1426
- const cached = join4(cacheDir(), binName);
1427
- if (existsSync5(cached)) return cached;
1522
+ const cached = join5(cacheDir(), binName);
1523
+ if (existsSync6(cached)) return cached;
1428
1524
  return download(log2);
1429
1525
  }
1430
1526
  async function openTunnel(localUrl, options2 = {}) {
@@ -1480,16 +1576,16 @@ function tunnelBanner(publicUrl, localUrl) {
1480
1576
 
1481
1577
  // src/commands/deploy.ts
1482
1578
  import { randomBytes as randomBytes2 } from "crypto";
1483
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1484
- import { join as join6 } from "path";
1579
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1580
+ import { join as join7 } from "path";
1485
1581
  import { createZoneClient } from "@flaredev/core/security";
1486
1582
  import { createJiti as createJiti2 } from "jiti";
1487
1583
  import pc3 from "picocolors";
1488
1584
 
1489
1585
  // src/utils/wrangler.ts
1490
1586
  import { spawn as spawn2 } from "child_process";
1491
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1492
- import { join as join5 } from "path";
1587
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1588
+ import { join as join6 } from "path";
1493
1589
  import { parse as parseJsonc } from "jsonc-parser";
1494
1590
  function runWrangler(wranglerBin, args, cwd, options2 = {}) {
1495
1591
  return new Promise((resolve6, reject) => {
@@ -1514,9 +1610,9 @@ ${output.trim()}`);
1514
1610
  return JSON.parse(output.slice(start, end + 1));
1515
1611
  }
1516
1612
  function readD1Databases(appRoot) {
1517
- const path = join5(appRoot, "wrangler.jsonc");
1518
- if (!existsSync6(path)) return [];
1519
- const config = parseJsonc(readFileSync3(path, "utf8"));
1613
+ const path = join6(appRoot, "wrangler.jsonc");
1614
+ if (!existsSync7(path)) return [];
1615
+ const config = parseJsonc(readFileSync4(path, "utf8"));
1520
1616
  return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
1521
1617
  }
1522
1618
 
@@ -1534,6 +1630,7 @@ function parseDeployArgs(args) {
1534
1630
  if (arg === "--skip-migrations") skipMigrations = true;
1535
1631
  else if (arg === "--skip-secrets") skipSecrets = true;
1536
1632
  else if (arg === "--skip-security") skipSecurity = true;
1633
+ else if (arg === "--yes" || arg === "-y") continue;
1537
1634
  else {
1538
1635
  forwarded.push(arg);
1539
1636
  if (arg === "--env" && args[i + 1]) env = args[i + 1];
@@ -1555,15 +1652,15 @@ function declaredSecretNames(devVarsExample) {
1555
1652
  return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
1556
1653
  }
1557
1654
  async function loadSecurityConfig(appRoot) {
1558
- const path = join6(appRoot, "security.config.ts");
1559
- if (!existsSync7(path)) return null;
1560
- const jiti = createJiti2(join6(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1655
+ const path = join7(appRoot, "security.config.ts");
1656
+ if (!existsSync8(path)) return null;
1657
+ const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1561
1658
  const mod = await jiti.import(path);
1562
1659
  if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
1563
1660
  return mod.default;
1564
1661
  }
1565
1662
  async function provisionZoneSecurity(appRoot, config, credentials, options2) {
1566
- const app = JSON.parse(readFileSync4(join6(appRoot, "package.json"), "utf8")).name ?? "app";
1663
+ const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
1567
1664
  const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
1568
1665
  if (config.zone?.rateLimits) {
1569
1666
  const result = await zone.syncRateLimits(config.zone.rateLimits);
@@ -1583,7 +1680,7 @@ async function runDeploy(args, context3) {
1583
1680
  const envArgs = plan.env ? ["--env", plan.env] : [];
1584
1681
  const step = (message) => log2(`
1585
1682
  ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1586
- const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1683
+ const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
1587
1684
  const migrateAfterDeploy = [];
1588
1685
  const migrate2 = async (db) => {
1589
1686
  step(`Applying migrations to ${db.database_name} (remote)`);
@@ -1642,8 +1739,8 @@ ${put.output.trim()}`);
1642
1739
  existing.add(name);
1643
1740
  }
1644
1741
  }
1645
- const examplePath = join6(appRoot, ".dev.vars.example");
1646
- const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1742
+ const examplePath = join7(appRoot, ".dev.vars.example");
1743
+ const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1647
1744
  if (optional.length > 0) {
1648
1745
  step("Optional secrets not set in production (features using them stay off):");
1649
1746
  for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
@@ -1678,25 +1775,30 @@ function findAppRoot(cwd) {
1678
1775
  while (dir) {
1679
1776
  const root = findUp("package.json", dir);
1680
1777
  if (!root) break;
1681
- const pkg = JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
1682
- if (pkg.dependencies?.vinext || pkg.devDependencies?.vinext) return root;
1778
+ const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
1779
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1780
+ if (deps["@flaredev/core"] || deps.vinext) return root;
1683
1781
  const parent = dirname4(root);
1684
1782
  dir = parent === root ? void 0 : parent;
1685
1783
  }
1686
- throw new Error("Not inside a Flare app (no package.json depending on vinext found). Run this from your app directory.");
1784
+ throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
1687
1785
  }
1688
1786
  function resolveBin(appRoot, pkgName, binName2) {
1689
- const pkgDir = findUp(join7("node_modules", pkgName, "package.json"), appRoot);
1787
+ const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
1690
1788
  if (!pkgDir) {
1691
1789
  throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
1692
1790
  }
1693
- const manifestPath = join7(pkgDir, "node_modules", pkgName, "package.json");
1694
- const { bin } = JSON.parse(readFileSync5(manifestPath, "utf8"));
1791
+ const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
1792
+ const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
1695
1793
  const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
1696
1794
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1697
- return join7(dirname4(manifestPath), relativeBin);
1795
+ return join8(dirname4(manifestPath), relativeBin);
1698
1796
  }
1699
1797
  function delegatedArgv(appRoot, command, forwarded) {
1798
+ if (readStack(appRoot) === "next") {
1799
+ const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
1800
+ throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
1801
+ }
1700
1802
  const spec = DELEGATED_COMMANDS[command];
1701
1803
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1702
1804
  }
@@ -1755,7 +1857,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
1755
1857
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1756
1858
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1757
1859
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1758
- if (command === "start" && !wantsHelp && !existsSync8(join7(appRoot, "dist/server/wrangler.json"))) {
1860
+ if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
1759
1861
  console.log("No production build found; running `flare build` first.\n");
1760
1862
  const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1761
1863
  if (code !== 0) return code;
@@ -1805,9 +1907,9 @@ function snapshotColumns(snapshot) {
1805
1907
  }
1806
1908
 
1807
1909
  // src/generator/migrations.ts
1808
- var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1910
+ var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1809
1911
  async function generateSchemaMigration(appRoot, name, options2 = {}) {
1810
- const dir = join8(appRoot, "migrations");
1912
+ const dir = join9(appRoot, "migrations");
1811
1913
  const before = new Set(listSql(dir));
1812
1914
  const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
1813
1915
  const { code, output } = await new Promise((resolve6, reject) => {
@@ -1835,10 +1937,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1835
1937
  for (const file of created) {
1836
1938
  const previous = previousSnapshot(dir, file);
1837
1939
  if (!previous) continue;
1838
- const path = join8(dir, file);
1839
- const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
1940
+ const path = join9(dir, file);
1941
+ const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
1840
1942
  if (changes.length) {
1841
- writeFileSync5(path, sql2);
1943
+ writeFileSync6(path, sql2);
1842
1944
  repairs.push(...changes.map((change) => `${file}: ${change}`));
1843
1945
  }
1844
1946
  }
@@ -1846,32 +1948,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1846
1948
  }
1847
1949
  function previousSnapshot(dir, migrationFile) {
1848
1950
  const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
1849
- const metaDir = join8(dir, "meta");
1850
- if (!prefix || !existsSync9(metaDir)) return void 0;
1951
+ const metaDir = join9(dir, "meta");
1952
+ if (!prefix || !existsSync10(metaDir)) return void 0;
1851
1953
  const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
1852
- return earlier ? JSON.parse(readFileSync6(join8(metaDir, earlier), "utf8")) : void 0;
1954
+ return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
1853
1955
  }
1854
1956
 
1855
1957
  // src/generator/plan.ts
1856
- import { copyFileSync as copyFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1857
- import { dirname as dirname5, join as join10 } from "path";
1958
+ import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
1959
+ import { dirname as dirname5, join as join11 } from "path";
1858
1960
  import pc5 from "picocolors";
1859
1961
 
1860
1962
  // src/generator/policy.ts
1861
- import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1862
- import { join as join9, resolve as resolve5 } from "path";
1963
+ import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
1964
+ import { join as join10, resolve as resolve5 } from "path";
1863
1965
  import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
1864
1966
  import { createJiti as createJiti3 } from "jiti";
1865
1967
  var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
1866
1968
  var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
1867
1969
  async function loadPolicies(root) {
1868
1970
  const appRoot = resolve5(root);
1869
- const dir = join9(appRoot, "policies");
1870
- if (!existsSync10(dir)) return [];
1871
- const jiti = createJiti3(join9(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1971
+ const dir = join10(appRoot, "policies");
1972
+ if (!existsSync11(dir)) return [];
1973
+ const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1872
1974
  const loaded = [];
1873
1975
  for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
1874
- const mod = await jiti.import(join9(dir, file));
1976
+ const mod = await jiti.import(join10(dir, file));
1875
1977
  const policy = mod.default;
1876
1978
  if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
1877
1979
  throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
@@ -1916,15 +2018,136 @@ function renderPolicyRegistry(all) {
1916
2018
  ].join("\n");
1917
2019
  }
1918
2020
 
1919
- // src/generator/render.ts
1920
- import { camelCase as camelCase3, columnName, relationGraph, storedFields } from "@flaredev/core";
1921
- var resourceLocal = (stem) => `${camelCase3(stem)}Resource`;
1922
- var tableExport = (resource) => camelCase3(resource.table);
2021
+ // src/generator/prisma.ts
2022
+ import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
1923
2023
  var q = (value) => JSON.stringify(value);
2024
+ var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
2025
+ var enumMember = (option) => {
2026
+ const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
2027
+ return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
2028
+ };
2029
+ function prismaType(resource, key, def) {
2030
+ switch (def.kind) {
2031
+ case "int":
2032
+ return "Int";
2033
+ case "float":
2034
+ return "Float";
2035
+ case "boolean":
2036
+ return "Boolean";
2037
+ case "date":
2038
+ case "datetime":
2039
+ return "DateTime";
2040
+ case "enum":
2041
+ return enumTypeName(resource.name, key);
2042
+ case "multiselect":
2043
+ return "String[]";
2044
+ default:
2045
+ return "String";
2046
+ }
2047
+ }
2048
+ function attributes(key, def) {
2049
+ const parts = [];
2050
+ const column = columnName(key);
2051
+ if (column !== key) parts.push(`@map(${q(column)})`);
2052
+ if ("unique" in def && def.unique) parts.push("@unique");
2053
+ if (def.kind === "text") parts.push("@db.Text");
2054
+ if (def.kind === "date") parts.push("@db.Date");
2055
+ if ("default" in def && def.default !== void 0) {
2056
+ const value = def.default;
2057
+ if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
2058
+ else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
2059
+ else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
2060
+ }
2061
+ return parts;
2062
+ }
2063
+ var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
2064
+ function renderPrismaEnums(resource) {
2065
+ return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
2066
+ if (def.kind !== "enum") return "";
2067
+ return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
2068
+ }).filter(Boolean);
2069
+ }
2070
+ function backReferences(target, all) {
2071
+ const found = [];
2072
+ for (const { resource: source } of all) {
2073
+ const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
2074
+ const ambiguous = pointing.length > 1;
2075
+ for (const [key, def] of pointing) {
2076
+ if (def.kind !== "belongsTo") continue;
2077
+ const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
2078
+ const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
2079
+ found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
2080
+ }
2081
+ }
2082
+ return found;
2083
+ }
2084
+ function renderPrismaModel(entry, all) {
2085
+ const resource = entry.resource;
2086
+ const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
2087
+ const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
2088
+ const indexes = [];
2089
+ for (const [key, def] of Object.entries(resource.fields)) {
2090
+ if (def.kind === "hasMany") {
2091
+ if (!byName.get(def.target)) {
2092
+ throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2093
+ }
2094
+ continue;
2095
+ }
2096
+ if (def.kind === "belongsTo") {
2097
+ const target = byName.get(def.target);
2098
+ if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2099
+ const optional2 = def.required ? "" : "?";
2100
+ const relationField = key.replace(/Id$/, "");
2101
+ const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
2102
+ const relationArgs = [
2103
+ ...named?.relationName ? [q(named.relationName)] : [],
2104
+ `fields: [${key}]`,
2105
+ "references: [id]",
2106
+ `onDelete: ${onDeleteAction(def.onDelete)}`
2107
+ ];
2108
+ lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
2109
+ lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
2110
+ indexes.push(` @@index([${key}])`);
2111
+ continue;
2112
+ }
2113
+ const optional = def.required || def.kind === "multiselect" ? "" : "?";
2114
+ lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
2115
+ }
2116
+ for (const back of backReferences(resource, all)) {
2117
+ const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
2118
+ lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
2119
+ }
2120
+ lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
2121
+ lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
2122
+ indexes.push(` @@index([createdAt])`);
2123
+ const sortField = resource.defaultSort.field;
2124
+ if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
2125
+ for (const [key, def] of storedFields(resource)) {
2126
+ if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
2127
+ }
2128
+ lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
2129
+ return lines.join("\n");
2130
+ }
2131
+ function renderPrismaSchema(all) {
2132
+ const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2133
+ const blocks = [];
2134
+ for (const entry of ordered) {
2135
+ blocks.push(...renderPrismaEnums(entry.resource));
2136
+ blocks.push(renderPrismaModel(entry, all));
2137
+ }
2138
+ return `${blocks.join("\n\n")}
2139
+ `;
2140
+ }
2141
+
2142
+ // src/generator/render.ts
2143
+ import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
2144
+ var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
2145
+ var tableExport = (resource) => camelCase4(resource.table);
2146
+ var q2 = (value) => JSON.stringify(value);
1924
2147
  var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
1925
2148
  var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
1926
2149
  function columnExpression(key, def, resources, selfName) {
1927
- const col = q(columnName(key));
2150
+ const col = q2(columnName2(key));
1928
2151
  let expr;
1929
2152
  switch (def.kind) {
1930
2153
  case "int":
@@ -1937,10 +2160,10 @@ function columnExpression(key, def, resources, selfName) {
1937
2160
  expr = `integer(${col}, { mode: "boolean" })`;
1938
2161
  break;
1939
2162
  case "enum":
1940
- expr = `text(${col}, { enum: ${q(def.options)} })`;
2163
+ expr = `text(${col}, { enum: ${q2(def.options)} })`;
1941
2164
  break;
1942
2165
  case "multiselect":
1943
- expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q(option)).join(" | ")})[]>()`;
2166
+ expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
1944
2167
  break;
1945
2168
  case "belongsTo": {
1946
2169
  const target = resources.get(def.target);
@@ -1948,7 +2171,7 @@ function columnExpression(key, def, resources, selfName) {
1948
2171
  throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
1949
2172
  }
1950
2173
  const onDelete = def.onDelete ?? "restrict";
1951
- expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q(onDelete)} })`;
2174
+ expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
1952
2175
  break;
1953
2176
  }
1954
2177
  default:
@@ -1956,13 +2179,13 @@ function columnExpression(key, def, resources, selfName) {
1956
2179
  }
1957
2180
  if (def.required) expr += ".notNull()";
1958
2181
  if ("unique" in def && def.unique) expr += ".unique()";
1959
- if ("default" in def && def.default !== void 0) expr += `.default(${q(def.default)})`;
2182
+ if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
1960
2183
  return expr;
1961
2184
  }
1962
2185
  function renderTableModule(entry, all) {
1963
2186
  const { resource } = entry;
1964
2187
  const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
1965
- const fields = storedFields(resource);
2188
+ const fields = storedFields2(resource);
1966
2189
  const name = tableExport(resource);
1967
2190
  const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
1968
2191
  const extras = [];
@@ -1970,12 +2193,12 @@ function renderTableModule(entry, all) {
1970
2193
  const addIndex = (key, column) => {
1971
2194
  if (indexed.has(key)) return;
1972
2195
  indexed.add(key);
1973
- extras.push(` index(${q(`${resource.table}_${columnName(column)}_idx`)}).on(table.${key}),`);
2196
+ extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
1974
2197
  };
1975
2198
  for (const [key, def] of fields) {
1976
2199
  if (def.kind === "enum") {
1977
2200
  const options2 = def.options.map(sqlString).join(", ");
1978
- extras.push(` check(${q(`${resource.table}_${columnName(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
2201
+ extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
1979
2202
  if (def.filterable !== false) addIndex(key, key);
1980
2203
  }
1981
2204
  if (def.kind === "belongsTo") addIndex(key, key);
@@ -1997,7 +2220,7 @@ function renderTableModule(entry, all) {
1997
2220
  ...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
1998
2221
  "",
1999
2222
  `export const ${name} = sqliteTable(`,
2000
- ` ${q(resource.table)},`,
2223
+ ` ${q2(resource.table)},`,
2001
2224
  " {",
2002
2225
  ` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
2003
2226
  ...columns.map(
@@ -2037,12 +2260,12 @@ function renderRelations(all) {
2037
2260
  const target = byName.get(relation.target);
2038
2261
  used.add(target);
2039
2262
  const other = tableExport(target);
2040
- return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q(relation.relationName)} }),`;
2263
+ return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
2041
2264
  }),
2042
2265
  ...hasMany.map((relation) => {
2043
2266
  const target = byName.get(relation.target);
2044
2267
  used.add(target);
2045
- return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q(relation.relationName)} }),`;
2268
+ return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
2046
2269
  })
2047
2270
  ];
2048
2271
  const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
@@ -2055,32 +2278,40 @@ ${lines.join("\n")}
2055
2278
  const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
2056
2279
  return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
2057
2280
  }
2058
- var handlerImports = (entry) => [
2059
- `import { createResourceHandlers } from "@flaredev/core/server";`,
2060
- `import { getDb } from "@/db";`,
2061
- `import { ${tableExport(entry.resource)} } from "@/db/schema";`,
2062
- `import { authorize, currentUser } from "@/lib/api";`,
2063
- `import { revalidateResource } from "@/lib/cache";`,
2064
- `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`,
2065
- "",
2066
- `const handlers = createResourceHandlers({`,
2067
- ` resource: ${resourceLocal(entry.stem)},`,
2068
- ` table: ${tableExport(entry.resource)},`,
2069
- ` getDb,`,
2070
- ` authorize,`,
2071
- ` currentUser,`,
2072
- ` onChange: revalidateResource,`,
2073
- `});`,
2074
- ""
2075
- ];
2076
- function renderCollectionRoute(entry) {
2077
- return [...handlerImports(entry), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
2281
+ var handlerImports = (entry, stack = "cloudflare") => {
2282
+ const local = resourceLocal(entry.stem);
2283
+ const source = stack === "next" ? {
2284
+ imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
2285
+ wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2286
+ } : {
2287
+ imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2288
+ wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
2289
+ };
2290
+ return [
2291
+ `import { createResourceHandlers } from "@flaredev/core/server";`,
2292
+ ...source.imports,
2293
+ `import { authorize, currentUser } from "@/lib/api";`,
2294
+ `import { revalidateResource } from "@/lib/cache";`,
2295
+ `import ${local} from "@/resources/${entry.stem}.resource";`,
2296
+ "",
2297
+ `const handlers = createResourceHandlers({`,
2298
+ ` resource: ${local},`,
2299
+ ...source.wiring,
2300
+ ` authorize,`,
2301
+ ` currentUser,`,
2302
+ ` onChange: revalidateResource,`,
2303
+ `});`,
2304
+ ""
2305
+ ];
2306
+ };
2307
+ function renderCollectionRoute(entry, stack = "cloudflare") {
2308
+ return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
2078
2309
  "\n"
2079
2310
  );
2080
2311
  }
2081
- function renderItemRoute(entry) {
2312
+ function renderItemRoute(entry, stack = "cloudflare") {
2082
2313
  return [
2083
- ...handlerImports(entry),
2314
+ ...handlerImports(entry, stack),
2084
2315
  "export const GET = handlers.item.GET;",
2085
2316
  "export const PATCH = handlers.item.PATCH;",
2086
2317
  "export const PUT = handlers.item.PUT;",
@@ -2159,7 +2390,7 @@ function renderClient(entry) {
2159
2390
  `import { createResourceClient } from "@flaredev/core/client";`,
2160
2391
  `import type ${local} from "./${stem}.resource";`,
2161
2392
  "",
2162
- `export const ${camelCase3(stem)}Client = createResourceClient<typeof ${local}>(${q(`/api/${resource.slug}`)});`,
2393
+ `export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
2163
2394
  "",
2164
2395
  `export type ${resource.name} = typeof ${local}.$types.record;`,
2165
2396
  `export type ${resource.name}Create = typeof ${local}.$types.create;`,
@@ -2173,7 +2404,7 @@ function renderValidators(entry) {
2173
2404
  `import { createValidators } from "@flaredev/core";`,
2174
2405
  `import ${local} from "./${entry.stem}.resource";`,
2175
2406
  "",
2176
- `export const ${camelCase3(entry.stem)}Validators = createValidators(${local});`,
2407
+ `export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
2177
2408
  ""
2178
2409
  ].join("\n");
2179
2410
  }
@@ -2203,12 +2434,31 @@ function renderServerRegistry(all) {
2203
2434
  ""
2204
2435
  ].join("\n");
2205
2436
  }
2206
- function resourceFiles(entry, all) {
2437
+ function renderPrismaServerRegistry(all) {
2438
+ if (all.length === 0) return "export const resourceTables = {} as const;\n";
2439
+ const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2440
+ return [
2441
+ `import { prisma } from "@/lib/db";`,
2442
+ `import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
2443
+ "",
2444
+ "export const resourceTables = {",
2445
+ ...sorted.map(
2446
+ ({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
2447
+ ),
2448
+ "} as const;",
2449
+ "",
2450
+ "export type ResourceName = keyof typeof resourceTables;",
2451
+ ""
2452
+ ].join("\n");
2453
+ }
2454
+ function resourceFiles(entry, all, stack = "cloudflare") {
2207
2455
  const { resource, stem } = entry;
2208
2456
  return [
2209
- { path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) },
2210
- { path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry) },
2211
- { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry) },
2457
+ // The schema is the one file that belongs to a stack. On Next.js every model lives
2458
+ // in one prisma/schema.prisma, written by planFiles rather than per resource.
2459
+ ...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
2460
+ { path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
2461
+ { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
2212
2462
  { path: `resources/${stem}.client.ts`, content: renderClient(entry) },
2213
2463
  { path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
2214
2464
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
@@ -2223,21 +2473,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
2223
2473
  // rewritten on regeneration; add your own code outside it.
2224
2474
  `;
2225
2475
  var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
2226
- function planFiles(all, policies) {
2476
+ function planFiles(all, policies, stack = "cloudflare") {
2227
2477
  const files = [];
2228
2478
  for (const entry of all) {
2229
- for (const file of resourceFiles(entry, all)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
2230
- }
2231
- files.push(
2232
- { path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
2233
- { path: "resources/index.ts", content: renderRegistry(all), header: "// Registry of every resource descriptor (maintained by flare gen).\n" },
2234
- {
2235
- path: "resources/server.ts",
2236
- content: renderServerRegistry(all),
2237
- header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
2238
- },
2239
- { path: "db/schema.ts", content: renderSchemaIndex(all) }
2240
- );
2479
+ for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
2480
+ }
2481
+ files.push({
2482
+ path: "resources/index.ts",
2483
+ content: renderRegistry(all),
2484
+ header: "// Registry of every resource descriptor (maintained by flare gen).\n"
2485
+ });
2486
+ if (stack === "next") {
2487
+ files.push(
2488
+ // Prisma keeps every model in one schema and writes its own migrations, so there
2489
+ // is no per-table module and no relations file to maintain.
2490
+ {
2491
+ path: "prisma/schema/resources.prisma",
2492
+ content: renderPrismaSchema(all),
2493
+ header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
2494
+ },
2495
+ {
2496
+ path: "resources/server.ts",
2497
+ content: renderPrismaServerRegistry(all),
2498
+ header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
2499
+ }
2500
+ );
2501
+ } else {
2502
+ files.push(
2503
+ { path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
2504
+ {
2505
+ path: "resources/server.ts",
2506
+ content: renderServerRegistry(all),
2507
+ header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
2508
+ },
2509
+ { path: "db/schema.ts", content: renderSchemaIndex(all) }
2510
+ );
2511
+ }
2241
2512
  if (policies) {
2242
2513
  files.push({
2243
2514
  path: "policies/index.ts",
@@ -2249,7 +2520,7 @@ function planFiles(all, policies) {
2249
2520
  }
2250
2521
  function applyPlan(appRoot, plan, options2 = {}) {
2251
2522
  return plan.map(({ path, content, header }) => {
2252
- const absolute = join10(appRoot, path);
2523
+ const absolute = join11(appRoot, path);
2253
2524
  const status = inspectGenerated(absolute, content);
2254
2525
  if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
2255
2526
  if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
@@ -2268,30 +2539,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
2268
2539
  function findOrphans(appRoot, all) {
2269
2540
  const planned = new Set(planFiles(all).map((file) => file.path));
2270
2541
  const candidates = [];
2271
- const list2 = (dir, match) => existsSync11(join10(appRoot, dir)) ? readdirSync6(join10(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2542
+ const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2272
2543
  candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
2273
2544
  candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
2274
2545
  for (const dir of list2("app/api", () => true)) {
2275
- for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
2546
+ for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2276
2547
  }
2277
2548
  for (const dir of list2("app/dashboard", () => true)) {
2278
2549
  for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2279
- if (existsSync11(join10(appRoot, file))) candidates.push(file);
2550
+ if (existsSync12(join11(appRoot, file))) candidates.push(file);
2280
2551
  }
2281
2552
  }
2282
2553
  const orphans = [];
2283
2554
  for (const path of candidates) {
2284
- const match = HEADER_PATTERN.exec(readFileSync7(join10(appRoot, path), "utf8"));
2555
+ const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
2285
2556
  if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
2286
2557
  }
2287
2558
  return orphans;
2288
2559
  }
2289
2560
  function ensureSupportFiles(appRoot, log2) {
2290
2561
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2291
- const target = join10(appRoot, path);
2292
- if (existsSync11(target)) continue;
2562
+ const target = join11(appRoot, path);
2563
+ if (existsSync12(target)) continue;
2293
2564
  mkdirSync4(dirname5(target), { recursive: true });
2294
- copyFileSync2(join10(templatesDir, "app", path), target);
2565
+ copyFileSync2(join11(templatesDir, "app", path), target);
2295
2566
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2296
2567
  }
2297
2568
  }
@@ -2319,15 +2590,15 @@ function billingFiles(appName) {
2319
2590
  }
2320
2591
  var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
2321
2592
  function appendVars(appRoot, target) {
2322
- const path = join11(appRoot, target);
2323
- const text = existsSync12(path) ? readFileSync8(path, "utf8") : "";
2593
+ const path = join12(appRoot, target);
2594
+ const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
2324
2595
  if (/STRIPE_SECRET_KEY/.test(text)) return false;
2325
2596
  appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
2326
2597
  return true;
2327
2598
  }
2328
2599
  function addStripeDependency(appRoot) {
2329
- const path = join11(appRoot, "package.json");
2330
- const pkg = JSON.parse(readFileSync8(path, "utf8"));
2600
+ const path = join12(appRoot, "package.json");
2601
+ const pkg = JSON.parse(readFileSync9(path, "utf8"));
2331
2602
  pkg.dependencies ??= {};
2332
2603
  if (pkg.dependencies.stripe) return false;
2333
2604
  pkg.dependencies.stripe = STRIPE_DEP;
@@ -2336,14 +2607,14 @@ function addStripeDependency(appRoot) {
2336
2607
  }
2337
2608
  function writeBillingResource(appRoot, name, force, log2) {
2338
2609
  const relative2 = descriptorPath(name);
2339
- const path = join11(appRoot, relative2);
2340
- if (!existsSync12(path)) {
2610
+ const path = join12(appRoot, relative2);
2611
+ if (!existsSync13(path)) {
2341
2612
  mkdirSync5(dirname6(path), { recursive: true });
2342
- writeFileSync6(path, renderBillingDescriptor(name));
2613
+ writeFileSync7(path, renderBillingDescriptor(name));
2343
2614
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2344
2615
  return;
2345
2616
  }
2346
- const parts = splitMarkers(readFileSync8(path, "utf8"));
2617
+ const parts = splitMarkers(readFileSync9(path, "utf8"));
2347
2618
  if (!parts) throw new Error(`${relative2} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
2348
2619
  const merged = mergeBillingFields(name, parts.block);
2349
2620
  if (!merged) {
@@ -2367,26 +2638,26 @@ async function genBilling(options2 = {}) {
2367
2638
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2368
2639
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2369
2640
  const relative2 = policyPath(name);
2370
- if (existsSync12(join11(appRoot, relative2))) {
2641
+ if (existsSync13(join12(appRoot, relative2))) {
2371
2642
  log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2372
2643
  continue;
2373
2644
  }
2374
- mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2375
- writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2645
+ mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2646
+ writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2376
2647
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2377
2648
  }
2378
2649
  const files = [];
2379
- const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2650
+ const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2380
2651
  for (const { path, content } of billingFiles(appName)) {
2381
- const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2652
+ const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
2382
2653
  files.push({ path, status: "missing", outcome });
2383
2654
  const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2384
2655
  log2(`${label.padEnd(18)} ${path}`);
2385
2656
  }
2386
2657
  for (const path of RETIRED_FILES) {
2387
- const absolute = join11(appRoot, path);
2388
- if (!existsSync12(absolute)) continue;
2389
- const parts = splitMarkers(readFileSync8(absolute, "utf8"));
2658
+ const absolute = join12(appRoot, path);
2659
+ if (!existsSync13(absolute)) continue;
2660
+ const parts = splitMarkers(readFileSync9(absolute, "utf8"));
2390
2661
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2391
2662
  if (untouched) {
2392
2663
  unlinkSync(absolute);
@@ -2407,7 +2678,7 @@ async function genBilling(options2 = {}) {
2407
2678
  }
2408
2679
  const all = await loadResources(appRoot);
2409
2680
  ensureSupportFiles(appRoot, log2);
2410
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2681
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
2411
2682
  let migrations = [];
2412
2683
  if (!options2.skipMigration) {
2413
2684
  const result = await generateSchemaMigration(appRoot, "billing");
@@ -2430,8 +2701,8 @@ async function genBilling(options2 = {}) {
2430
2701
  }
2431
2702
 
2432
2703
  // src/commands/gen-security.ts
2433
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2434
- import { dirname as dirname7, join as join12 } from "path";
2704
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
2705
+ import { dirname as dirname7, join as join13 } from "path";
2435
2706
  import pc7 from "picocolors";
2436
2707
 
2437
2708
  // src/generator/security.ts
@@ -2959,28 +3230,28 @@ function securityFiles(appName) {
2959
3230
  }
2960
3231
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2961
3232
  function writeOnce(appRoot, relative2, content, log2) {
2962
- const path = join12(appRoot, relative2);
2963
- if (existsSync13(path)) {
3233
+ const path = join13(appRoot, relative2);
3234
+ if (existsSync14(path)) {
2964
3235
  log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2965
3236
  return;
2966
3237
  }
2967
3238
  mkdirSync6(dirname7(path), { recursive: true });
2968
- writeFileSync7(path, content);
3239
+ writeFileSync8(path, content);
2969
3240
  log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
2970
3241
  }
2971
3242
  async function genSecurity(options2 = {}) {
2972
3243
  const log2 = options2.log ?? ((message) => console.log(message));
2973
3244
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2974
- const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
3245
+ const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
2975
3246
  writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
2976
3247
  writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
2977
3248
  writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
2978
3249
  const files = [];
2979
3250
  const write = (path, content, header) => {
2980
- const absolute = join12(appRoot, path);
2981
- if (path === "lib/security.ts" && existsSync13(absolute)) {
2982
- const source = readFileSync9(absolute, "utf8");
2983
- if (PASS_THROUGH_HEADER.test(source)) writeFileSync7(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
3251
+ const absolute = join13(appRoot, path);
3252
+ if (path === "lib/security.ts" && existsSync14(absolute)) {
3253
+ const source = readFileSync10(absolute, "utf8");
3254
+ if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
2984
3255
  }
2985
3256
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
2986
3257
  files.push({ path, status: "missing", outcome });
@@ -2988,16 +3259,16 @@ async function genSecurity(options2 = {}) {
2988
3259
  log2(`${label.padEnd(18)} ${path}`);
2989
3260
  };
2990
3261
  for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
2991
- const navPath = join12(appRoot, "lib/dashboard-nav.ts");
2992
- if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
3262
+ const navPath = join13(appRoot, "lib/dashboard-nav.ts");
3263
+ if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
2993
3264
  else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
2994
- const wranglerPath = join12(appRoot, "wrangler.jsonc");
3265
+ const wranglerPath = join13(appRoot, "wrangler.jsonc");
2995
3266
  let bindings = [];
2996
- if (existsSync13(wranglerPath)) {
2997
- const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
3267
+ if (existsSync14(wranglerPath)) {
3268
+ const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
2998
3269
  bindings = added;
2999
3270
  if (added.length) {
3000
- writeFileSync7(wranglerPath, text);
3271
+ writeFileSync8(wranglerPath, text);
3001
3272
  for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
3002
3273
  }
3003
3274
  } else {
@@ -3005,7 +3276,7 @@ async function genSecurity(options2 = {}) {
3005
3276
  }
3006
3277
  const all = await loadResources(appRoot);
3007
3278
  ensureSupportFiles(appRoot, log2);
3008
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
3279
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
3009
3280
  let migrations = [];
3010
3281
  if (!options2.skipMigration) {
3011
3282
  const result = await generateSchemaMigration(appRoot, "security");
@@ -3028,15 +3299,15 @@ async function genSecurity(options2 = {}) {
3028
3299
  }
3029
3300
 
3030
3301
  // src/commands/theme.ts
3031
- import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3032
- import { join as join13 } from "path";
3302
+ import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3303
+ import { join as join14 } from "path";
3033
3304
  import pc8 from "picocolors";
3034
3305
  var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
3035
3306
  function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3036
3307
  const appRoot = findAppRoot(cwd);
3037
- const path = join13(appRoot, "lib/site.ts");
3038
- if (!existsSync14(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
3039
- const source = readFileSync10(path, "utf8");
3308
+ const path = join14(appRoot, "lib/site.ts");
3309
+ if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
3310
+ const source = readFileSync11(path, "utf8");
3040
3311
  const current = THEME_LINE.exec(source)?.[1];
3041
3312
  if (!name) {
3042
3313
  for (const [theme2, description] of Object.entries(THEMES)) {
@@ -3047,7 +3318,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3047
3318
  }
3048
3319
  const theme = parseTheme(name);
3049
3320
  if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
3050
- writeFileSync8(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3321
+ writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3051
3322
  log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3052
3323
  }
3053
3324
 
@@ -3119,9 +3390,9 @@ async function askCreateQuestions(given, appName) {
3119
3390
  }
3120
3391
 
3121
3392
  // src/commands/gen.ts
3122
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
3123
- import { dirname as dirname8, join as join14 } from "path";
3124
- import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3393
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
3394
+ import { dirname as dirname8, join as join15 } from "path";
3395
+ import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3125
3396
  import pc10 from "picocolors";
3126
3397
  function validateFields(name, fields, others, log2) {
3127
3398
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
@@ -3133,7 +3404,7 @@ function validateFields(name, fields, others, log2) {
3133
3404
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
3134
3405
  const graph = relationGraph2([...others, candidate]);
3135
3406
  for (const pending of graph.pending.filter((p) => p.resource === name)) {
3136
- log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
3407
+ log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
3137
3408
  }
3138
3409
  }
3139
3410
  async function genResource(rawName, options2) {
@@ -3141,8 +3412,8 @@ async function genResource(rawName, options2) {
3141
3412
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3142
3413
  const name = resourceName(rawName);
3143
3414
  const relativePath = descriptorPath(name);
3144
- const path = join14(appRoot, relativePath);
3145
- const exists = existsSync15(path);
3415
+ const path = join15(appRoot, relativePath);
3416
+ const exists = existsSync16(path);
3146
3417
  if (!exists && !options2.fields) {
3147
3418
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
3148
3419
  }
@@ -3153,7 +3424,7 @@ async function genResource(rawName, options2) {
3153
3424
  validateFields(name, fields, others, log2);
3154
3425
  if (!exists) {
3155
3426
  mkdirSync7(dirname8(path), { recursive: true });
3156
- writeFileSync9(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3427
+ writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3157
3428
  log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
3158
3429
  } else {
3159
3430
  try {
@@ -3175,7 +3446,7 @@ async function genResource(rawName, options2) {
3175
3446
  const all = await loadResources(appRoot);
3176
3447
  table = all.find(({ resource }) => resource.name === name).resource.table;
3177
3448
  ensureSupportFiles(appRoot, log2);
3178
- written = applyPlan(appRoot, planFiles(all), { force: options2.force });
3449
+ written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
3179
3450
  logResults(written, log2);
3180
3451
  } catch (error) {
3181
3452
  const hint = exists ? "" : `
@@ -3189,22 +3460,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
3189
3460
  }
3190
3461
  let migrations = [];
3191
3462
  if (!options2.skipMigration) {
3192
- const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3193
- migrations = result.files;
3194
- for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3195
- for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3196
- if (migrations.length) log2(`
3463
+ if (readStack(appRoot) === "next") {
3464
+ log2(`
3465
+ Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
3466
+ } else {
3467
+ const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3468
+ migrations = result.files;
3469
+ for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3470
+ for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3471
+ if (migrations.length) log2(`
3197
3472
  Next: ${pc10.bold("flare migrate")} to apply it locally.`);
3198
- else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3473
+ else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3474
+ }
3199
3475
  }
3200
3476
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
3201
3477
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
3202
3478
  }
3203
3479
 
3204
3480
  // src/commands/gen-endpoint.ts
3205
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
3206
- import { join as join15 } from "path";
3207
- import { kebabCase as kebabCase3, pascalCase as pascalCase2 } from "@flaredev/core";
3481
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
3482
+ import { join as join16 } from "path";
3483
+ import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
3208
3484
  import pc11 from "picocolors";
3209
3485
  var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
3210
3486
  var ACTIONS = ["read", "create", "update", "delete"];
@@ -3216,7 +3492,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3216
3492
  const name = kebabCase3(rawName);
3217
3493
  if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
3218
3494
  const resources = await loadResources(appRoot);
3219
- const wanted = pascalCase2(resourceName2);
3495
+ const wanted = pascalCase3(resourceName2);
3220
3496
  const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
3221
3497
  if (!entry) {
3222
3498
  throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
@@ -3225,11 +3501,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3225
3501
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3226
3502
  const { resource, stem } = entry;
3227
3503
  const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3228
- const path = join15(appRoot, relative2);
3229
- if (existsSync16(path)) throw new Error(`${relative2} already exists.`);
3230
- mkdirSync8(join15(path, ".."), { recursive: true });
3504
+ const path = join16(appRoot, relative2);
3505
+ if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
3506
+ mkdirSync8(join16(path, ".."), { recursive: true });
3231
3507
  const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3232
- writeFileSync10(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3508
+ writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3233
3509
  log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3234
3510
  log2(`
3235
3511
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
@@ -3265,8 +3541,8 @@ ${body}
3265
3541
  }
3266
3542
 
3267
3543
  // src/commands/gen-migration.ts
3268
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
3269
- import { basename as basename2, join as join16 } from "path";
3544
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
3545
+ import { basename as basename2, join as join17 } from "path";
3270
3546
  import { snakeCase } from "@flaredev/core";
3271
3547
  import pc12 from "picocolors";
3272
3548
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
@@ -3287,12 +3563,12 @@ async function genMigration(rawName, options2 = {}) {
3287
3563
  for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
3288
3564
  for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
3289
3565
  if (!options2.fromSchema) {
3290
- const downDir = join16(appRoot, "migrations", "down");
3566
+ const downDir = join17(appRoot, "migrations", "down");
3291
3567
  mkdirSync9(downDir, { recursive: true });
3292
3568
  for (const file of files) {
3293
- const down = join16(downDir, basename2(file));
3294
- if (existsSync17(down)) continue;
3295
- writeFileSync11(down, DOWN_TEMPLATE(basename2(file)));
3569
+ const down = join17(downDir, basename2(file));
3570
+ if (existsSync18(down)) continue;
3571
+ writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
3296
3572
  log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3297
3573
  }
3298
3574
  log2(`
@@ -3305,8 +3581,8 @@ Next: ${pc12.bold("flare migrate")}.`);
3305
3581
  }
3306
3582
 
3307
3583
  // src/commands/gen-policy.ts
3308
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
3309
- import { dirname as dirname9, join as join17 } from "path";
3584
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
3585
+ import { dirname as dirname9, join as join18 } from "path";
3310
3586
  import { definePolicy } from "@flaredev/core";
3311
3587
  import pc13 from "picocolors";
3312
3588
  function parseRoles(input, what = "--roles") {
@@ -3324,8 +3600,8 @@ async function genPolicy(rawName, options2) {
3324
3600
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3325
3601
  }
3326
3602
  const relative2 = policyPath(name);
3327
- const path = join17(appRoot, relative2);
3328
- const exists = existsSync18(path);
3603
+ const path = join18(appRoot, relative2);
3604
+ const exists = existsSync19(path);
3329
3605
  let roles;
3330
3606
  let deleteRoles;
3331
3607
  if (options2.roles || !exists) {
@@ -3337,7 +3613,7 @@ async function genPolicy(rawName, options2) {
3337
3613
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
3338
3614
  if (!exists) {
3339
3615
  mkdirSync10(dirname9(path), { recursive: true });
3340
- writeFileSync12(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3616
+ writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3341
3617
  log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3342
3618
  } else {
3343
3619
  try {
@@ -3352,16 +3628,16 @@ async function genPolicy(rawName, options2) {
3352
3628
  }
3353
3629
  }
3354
3630
  const policies = await loadPolicies(appRoot);
3355
- logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
3631
+ logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3356
3632
  log2(`
3357
3633
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3358
3634
  return { name, path: relative2 };
3359
3635
  }
3360
3636
 
3361
3637
  // src/commands/migrate.ts
3362
- import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
3638
+ import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
3363
3639
  import { tmpdir } from "os";
3364
- import { join as join18 } from "path";
3640
+ import { join as join19 } from "path";
3365
3641
  import pc14 from "picocolors";
3366
3642
 
3367
3643
  // src/generator/down.ts
@@ -3455,7 +3731,17 @@ function context(options2) {
3455
3731
  };
3456
3732
  }
3457
3733
  async function migrate(options2 = {}) {
3458
- const { appRoot, db, target, wrangler } = context(options2);
3734
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3735
+ const log2 = options2.log ?? ((message) => console.log(message));
3736
+ if (readStack(appRoot) === "next") {
3737
+ if (options2.remote) {
3738
+ log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
3739
+ }
3740
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
3741
+ log2(`${pc14.cyan("prisma")} migrate deploy`);
3742
+ return runNode([prisma, "migrate", "deploy"], appRoot);
3743
+ }
3744
+ const { db, target, wrangler } = context(options2);
3459
3745
  const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
3460
3746
  return result.code;
3461
3747
  }
@@ -3479,7 +3765,7 @@ async function rollback(options2 = {}) {
3479
3765
  const steps = options2.steps ?? 1;
3480
3766
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3481
3767
  const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
3482
- const migrationsDir = join18(appRoot, db.migrations_dir ?? "migrations");
3768
+ const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
3483
3769
  const applied = await appliedMigrations(options2, steps);
3484
3770
  if (applied.length === 0) {
3485
3771
  log2("Nothing to roll back.");
@@ -3488,18 +3774,18 @@ async function rollback(options2 = {}) {
3488
3774
  const plan = [];
3489
3775
  for (const { name } of applied) {
3490
3776
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
3491
- const handWritten = join18(migrationsDir, "down", name);
3492
- if (existsSync19(handWritten)) {
3493
- const sql2 = readFileSync11(handWritten, "utf8");
3777
+ const handWritten = join19(migrationsDir, "down", name);
3778
+ if (existsSync20(handWritten)) {
3779
+ const sql2 = readFileSync12(handWritten, "utf8");
3494
3780
  if (splitStatements(sql2).length === 0) {
3495
3781
  throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
3496
3782
  }
3497
3783
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3498
3784
  continue;
3499
3785
  }
3500
- const upPath = join18(migrationsDir, name);
3501
- if (!existsSync19(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3502
- const down = deriveDown(readFileSync11(upPath, "utf8"));
3786
+ const upPath = join19(migrationsDir, name);
3787
+ if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3788
+ const down = deriveDown(readFileSync12(upPath, "utf8"));
3503
3789
  if (!down.reversible) {
3504
3790
  throw new Error(
3505
3791
  `Can't roll back ${name}: ${down.reason}
@@ -3519,15 +3805,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3519
3805
  }
3520
3806
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3521
3807
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3522
- const dir = mkdtempSync(join18(tmpdir(), "flare-rollback-"));
3808
+ const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
3523
3809
  try {
3524
- const file = join18(dir, "rollback.sql");
3525
- writeFileSync13(file, script);
3810
+ const file = join19(dir, "rollback.sql");
3811
+ writeFileSync14(file, script);
3526
3812
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3527
3813
  if (result.code !== 0) throw new Error(`Rollback failed:
3528
3814
  ${result.output.trim()}`);
3529
3815
  } finally {
3530
- rmSync2(dir, { recursive: true, force: true });
3816
+ rmSync3(dir, { recursive: true, force: true });
3531
3817
  }
3532
3818
  log2(pc14.green(`
3533
3819
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
@@ -3535,8 +3821,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3535
3821
  }
3536
3822
 
3537
3823
  // src/commands/rm.ts
3538
- import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3539
- import { dirname as dirname10, join as join19 } from "path";
3824
+ import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3825
+ import { dirname as dirname10, join as join20 } from "path";
3540
3826
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3541
3827
  import pc15 from "picocolors";
3542
3828
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
@@ -3555,10 +3841,10 @@ function handWrittenReason(source, header, options2 = {}) {
3555
3841
  return null;
3556
3842
  }
3557
3843
  function removeEmptyDirs(appRoot, start) {
3558
- let dir = dirname10(join19(appRoot, start));
3559
- const roots = [join19(appRoot, "app", "api"), join19(appRoot, "app", "dashboard")];
3844
+ let dir = dirname10(join20(appRoot, start));
3845
+ const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
3560
3846
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3561
- while (inside() && existsSync20(dir) && readdirSync7(dir).length === 0) {
3847
+ while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
3562
3848
  rmdirSync(dir);
3563
3849
  dir = dirname10(dir);
3564
3850
  }
@@ -3586,8 +3872,8 @@ async function rmResource(rawName, options2 = {}) {
3586
3872
  const owned = /* @__PURE__ */ new Set();
3587
3873
  if (entry) {
3588
3874
  owned.add(entry.file);
3589
- for (const file of planFiles(all)) {
3590
- if (existsSync20(join19(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3875
+ for (const file of planFiles(all, void 0, readStack(appRoot))) {
3876
+ if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3591
3877
  }
3592
3878
  }
3593
3879
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3596,7 +3882,7 @@ async function rmResource(rawName, options2 = {}) {
3596
3882
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3597
3883
  const findings = [];
3598
3884
  for (const path of owned) {
3599
- const source = readFileSync12(join19(appRoot, path), "utf8");
3885
+ const source = readFileSync13(join20(appRoot, path), "utf8");
3600
3886
  let wrapper;
3601
3887
  if (entry && path === entry.file) {
3602
3888
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3617,15 +3903,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
3617
3903
  }
3618
3904
  const removed = [...owned].sort();
3619
3905
  for (const path of removed) {
3620
- unlinkSync2(join19(appRoot, path));
3906
+ unlinkSync2(join20(appRoot, path));
3621
3907
  removeEmptyDirs(appRoot, path);
3622
3908
  log2(`${pc15.red("remove".padEnd(9))} ${path}`);
3623
3909
  }
3624
- logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
3910
+ logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
3625
3911
  const table = entry ? tableExport(entry.resource) : void 0;
3626
- if (table && existsSync20(join19(appRoot, "seeds"))) {
3627
- const seeds = readdirSync7(join19(appRoot, "seeds")).filter(
3628
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join19(appRoot, "seeds", file), "utf8"))
3912
+ if (table && existsSync21(join20(appRoot, "seeds"))) {
3913
+ const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
3914
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
3629
3915
  );
3630
3916
  if (seeds.length) log2(pc15.yellow(`
3631
3917
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
@@ -3695,9 +3981,9 @@ ${result.output.trim()}`);
3695
3981
  }
3696
3982
 
3697
3983
  // src/commands/seed.ts
3698
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "fs";
3699
- import { join as join21 } from "path";
3700
- import { createFake, kebabCase as kebabCase4, storedFields as storedFields2 } from "@flaredev/core";
3984
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
3985
+ import { join as join22 } from "path";
3986
+ import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
3701
3987
  import { createJiti as createJiti4 } from "jiti";
3702
3988
  import pc17 from "picocolors";
3703
3989
 
@@ -3794,16 +4080,16 @@ function createInsertMany(sink, drizzle, onProgress) {
3794
4080
 
3795
4081
  // src/seed/local-d1.ts
3796
4082
  import { createRequire } from "module";
3797
- import { join as join20 } from "path";
4083
+ import { join as join21 } from "path";
3798
4084
  import { pathToFileURL } from "url";
3799
4085
  var LOCAL_STATE = ".wrangler/state/v3";
3800
4086
  async function openLocalD1(appRoot, binding) {
3801
4087
  process.env.WRANGLER_LOG ??= "error";
3802
- const appRequire = createRequire(join20(appRoot, "package.json"));
4088
+ const appRequire = createRequire(join21(appRoot, "package.json"));
3803
4089
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3804
4090
  const { env, dispose } = await wrangler.getPlatformProxy({
3805
- configPath: join20(appRoot, "wrangler.jsonc"),
3806
- persist: { path: join20(appRoot, LOCAL_STATE) }
4091
+ configPath: join21(appRoot, "wrangler.jsonc"),
4092
+ persist: { path: join21(appRoot, LOCAL_STATE) }
3807
4093
  });
3808
4094
  const bindings = env;
3809
4095
  const name = binding ?? "DB";
@@ -3818,8 +4104,8 @@ async function openLocalD1(appRoot, binding) {
3818
4104
  // src/commands/seed.ts
3819
4105
  var SEEDS_DIR = "seeds";
3820
4106
  function seedFiles(appRoot, names) {
3821
- const dir = join21(appRoot, SEEDS_DIR);
3822
- const all = existsSync21(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4107
+ const dir = join22(appRoot, SEEDS_DIR);
4108
+ const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3823
4109
  if (!names?.length) return all;
3824
4110
  return names.map((name) => {
3825
4111
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3837,16 +4123,20 @@ async function runSeeds(options2 = {}) {
3837
4123
  log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
3838
4124
  return;
3839
4125
  }
3840
- const jiti = createJiti4(join21(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4126
+ if (readStack(appRoot) === "next") {
4127
+ await runPrismaSeeds(appRoot, files, log2);
4128
+ return;
4129
+ }
4130
+ const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3841
4131
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3842
4132
  const drizzleHelpers = await jiti.import("drizzle-orm");
3843
- const schema = await jiti.import(join21(appRoot, "db/schema.ts"));
4133
+ const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
3844
4134
  const local = await openLocalD1(appRoot);
3845
4135
  try {
3846
4136
  const db = drizzle(local.db, { schema });
3847
4137
  const insertMany = createInsertMany(local.db, drizzleHelpers);
3848
4138
  for (const file of files) {
3849
- const mod = await jiti.import(join21(appRoot, SEEDS_DIR, file));
4139
+ const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
3850
4140
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3851
4141
  log2(`${pc17.cyan("seed")} ${file}`);
3852
4142
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
@@ -3896,9 +4186,17 @@ function sampleValue(key, def) {
3896
4186
  return void 0;
3897
4187
  }
3898
4188
  }
3899
- function renderSeed(resource) {
4189
+ function renderSeed(resource, stack = "cloudflare") {
3900
4190
  if (!resource) {
3901
- return `import { defineSeed } from "@flaredev/core";
4191
+ return stack === "next" ? `import { defineSeed } from "@flaredev/core";
4192
+
4193
+ export default defineSeed(async ({ db, insertMany, fake, log }) => {
4194
+ // \`db\` is this app's Prisma client, e.g.:
4195
+ // await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
4196
+ // Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
4197
+ log("nothing to seed yet");
4198
+ });
4199
+ ` : `import { defineSeed } from "@flaredev/core";
3902
4200
 
3903
4201
  export default defineSeed(async ({ db, insertMany, fake, log }) => {
3904
4202
  // Insert rows with Drizzle, e.g.:
@@ -3911,11 +4209,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
3911
4209
  const table = tableExport(resource);
3912
4210
  const lines = [];
3913
4211
  const todos = [];
3914
- for (const [key, def] of storedFields2(resource)) {
4212
+ for (const [key, def] of storedFields3(resource)) {
3915
4213
  const value = sampleValue(key, def);
3916
4214
  if (value !== void 0) lines.push(` ${key}: ${value},`);
3917
4215
  else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
3918
4216
  }
4217
+ if (stack === "next") {
4218
+ const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
4219
+ return `import { defineSeed } from "@flaredev/core";
4220
+
4221
+ /** How many to create. createMany sends them in one statement. */
4222
+ const COUNT = 50;
4223
+
4224
+ export default defineSeed(async ({ insertMany, fake, log }) => {
4225
+ await insertMany(
4226
+ ${JSON.stringify(camelCase6(resource.name))},
4227
+ Array.from({ length: COUNT }, () => ({
4228
+ ${body}
4229
+ updatedAt: new Date(),
4230
+ })),
4231
+ );
4232
+ log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
4233
+ });
4234
+ `;
4235
+ }
3919
4236
  return `import { defineSeed } from "@flaredev/core";
3920
4237
  import { ${table} } from "@/db/schema";
3921
4238
 
@@ -3936,8 +4253,8 @@ async function makeSeed(rawName, options2 = {}) {
3936
4253
  const name = kebabCase4(rawName);
3937
4254
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3938
4255
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3939
- const path = join21(appRoot, relative2);
3940
- if (existsSync21(path)) throw new Error(`${relative2} already exists.`);
4256
+ const path = join22(appRoot, relative2);
4257
+ if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
3941
4258
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3942
4259
  let resource;
3943
4260
  if (options2.resource) {
@@ -3946,22 +4263,171 @@ async function makeSeed(rawName, options2 = {}) {
3946
4263
  } else {
3947
4264
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
3948
4265
  }
3949
- mkdirSync11(join21(appRoot, SEEDS_DIR), { recursive: true });
3950
- writeFileSync14(path, renderSeed(resource));
4266
+ mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4267
+ writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
3951
4268
  log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
3952
4269
  log2(`
3953
4270
  Run it with ${pc17.bold(`flare seed ${name}`)}.`);
3954
4271
  return relative2;
3955
4272
  }
4273
+ async function runPrismaSeeds(appRoot, files, log2) {
4274
+ const runner = join22(appRoot, ".flare-seed-runner.mts");
4275
+ const script = [
4276
+ `import "dotenv/config";`,
4277
+ `import { createFake } from "@flaredev/core";`,
4278
+ `import { prisma } from "./lib/db";`,
4279
+ ...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
4280
+ "",
4281
+ `const files = ${JSON.stringify(files)};`,
4282
+ `const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
4283
+ `const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
4284
+ "",
4285
+ `for (const [index, seed] of seeds.entries()) {`,
4286
+ ` console.log("seed " + files[index]);`,
4287
+ ` await seed({`,
4288
+ ` db: prisma,`,
4289
+ ` env: process.env,`,
4290
+ ` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
4291
+ ` const delegate = client[model];`,
4292
+ ` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
4293
+ ` await delegate.createMany({ data: rows });`,
4294
+ ` },`,
4295
+ ` fake: createFake(),`,
4296
+ ` log: (message: string) => console.log(" " + message),`,
4297
+ ` });`,
4298
+ `}`,
4299
+ `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4300
+ ""
4301
+ ].join("\n");
4302
+ writeFileSync15(runner, script);
4303
+ try {
4304
+ const tsx = resolveBin(appRoot, "tsx", "tsx");
4305
+ const code = await runNode([tsx, runner], appRoot);
4306
+ if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4307
+ log2(pc17.green(`
4308
+ Ran ${files.length} seed(s).`));
4309
+ } finally {
4310
+ rmSync4(runner, { force: true });
4311
+ }
4312
+ }
3956
4313
 
3957
- // src/commands/seed-resource.ts
3958
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4314
+ // src/commands/db-push.ts
4315
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
3959
4316
  import { tmpdir as tmpdir2 } from "os";
3960
- import { join as join22 } from "path";
4317
+ import { join as join23 } from "path";
3961
4318
  import pc18 from "picocolors";
3962
4319
 
4320
+ // src/terminal.ts
4321
+ var isTTY = () => Boolean(process.stdout.isTTY);
4322
+ function formatDuration(ms) {
4323
+ const seconds = Math.round(ms / 1e3);
4324
+ if (seconds < 60) return `${seconds}s`;
4325
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
4326
+ return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
4327
+ }
4328
+ var formatCount = (value) => value.toLocaleString();
4329
+ function formatRate(count, ms, unit = "rows") {
4330
+ if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
4331
+ return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
4332
+ }
4333
+ function progressLine(everyMs = 120) {
4334
+ if (!isTTY()) return { update: () => {
4335
+ }, done: () => {
4336
+ } };
4337
+ let last = 0;
4338
+ let width = 0;
4339
+ const write = (message) => {
4340
+ process.stdout.write(`\r${message.padEnd(width)}`);
4341
+ width = Math.max(width, message.length);
4342
+ };
4343
+ return {
4344
+ update(message) {
4345
+ const now = Date.now();
4346
+ if (now - last < everyMs) return;
4347
+ last = now;
4348
+ write(message);
4349
+ },
4350
+ done() {
4351
+ if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
4352
+ width = 0;
4353
+ }
4354
+ };
4355
+ }
4356
+
4357
+ // src/commands/db-push.ts
4358
+ var PAGE = 2e3;
4359
+ function pickDatabase(databases, wanted) {
4360
+ if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4361
+ if (wanted) {
4362
+ const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
4363
+ if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
4364
+ return match;
4365
+ }
4366
+ if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
4367
+ return databases[0];
4368
+ }
4369
+ async function dbPush(tables, options2 = {}) {
4370
+ const log2 = options2.log ?? ((message) => console.log(message));
4371
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4372
+ if (tables.length === 0) throw new Error("Name at least one table, e.g. flare db:push products categories --yes");
4373
+ const bad = tables.find((table) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(table));
4374
+ if (bad) throw new Error(`"${bad}" isn't a table name.`);
4375
+ if (!options2.yes) {
4376
+ throw new Error(`This writes ${tables.join(", ")} to the deployed database. Re-run with --yes to confirm.`);
4377
+ }
4378
+ const db = pickDatabase(readD1Databases(appRoot), options2.database);
4379
+ const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4380
+ const started = Date.now();
4381
+ const local = await openLocalD1(appRoot, options2.database);
4382
+ const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4383
+ try {
4384
+ const statements = [];
4385
+ const counts = [];
4386
+ for (const table of tables) {
4387
+ if (options2.truncate) statements.push(`DELETE FROM "${table}";`);
4388
+ let offset = 0;
4389
+ let rows = 0;
4390
+ for (; ; ) {
4391
+ const page = await local.db.prepare(`SELECT * FROM "${table}" LIMIT ${PAGE} OFFSET ${offset}`).all();
4392
+ const results = page.results ?? [];
4393
+ if (results.length === 0) break;
4394
+ const columns = Object.keys(results[0]);
4395
+ for (const statement of insertStatements(table, columns, results)) statements.push(statement.sql);
4396
+ rows += results.length;
4397
+ if (results.length < PAGE) break;
4398
+ offset += PAGE;
4399
+ }
4400
+ counts.push({ table, rows });
4401
+ log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
4402
+ }
4403
+ const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
4404
+ if (total === 0) {
4405
+ log2("Nothing to push: those tables are empty locally.");
4406
+ return { tables: counts, ms: Date.now() - started };
4407
+ }
4408
+ const file = join23(dir, "push.sql");
4409
+ writeFileSync16(file, statements.join("\n"));
4410
+ const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4411
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4412
+ if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
4413
+ ${result.output.trim()}`);
4414
+ const ms = Date.now() - started;
4415
+ log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4416
+ return { tables: counts, ms };
4417
+ } finally {
4418
+ rmSync5(dir, { recursive: true, force: true });
4419
+ await local.dispose();
4420
+ }
4421
+ }
4422
+
4423
+ // src/commands/seed-resource.ts
4424
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
4425
+ import { tmpdir as tmpdir3 } from "os";
4426
+ import { join as join24 } from "path";
4427
+ import pc19 from "picocolors";
4428
+
3963
4429
  // src/seed/rows.ts
3964
- import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
4430
+ import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
3965
4431
  var has = (key, ...words) => words.some((word) => key.includes(word));
3966
4432
  function stringValue(key, def, fake, resource, index) {
3967
4433
  switch (def.format) {
@@ -3981,7 +4447,9 @@ function stringValue(key, def, fake, resource, index) {
3981
4447
  return fake.slug();
3982
4448
  }
3983
4449
  const name = key.toLowerCase();
3984
- const organisation = has(resource.name.toLowerCase(), "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
4450
+ const resourceName2 = resource.name.toLowerCase();
4451
+ const organisation = has(resourceName2, "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
4452
+ const catalogue = has(resourceName2, "product", "item", "article", "sku", "listing", "merch", "book", "course", "plan", "service");
3985
4453
  let value;
3986
4454
  if (has(name, "email")) value = fake.email();
3987
4455
  else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
@@ -3996,7 +4464,7 @@ function stringValue(key, def, fake, resource, index) {
3996
4464
  else if (has(name, "colour", "color")) value = fake.color();
3997
4465
  else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
3998
4466
  else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
3999
- else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
4467
+ else if (has(name, "name")) value = organisation ? fake.company() : catalogue ? fake.product(digitalish(resource)) : fake.fullName();
4000
4468
  else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
4001
4469
  else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
4002
4470
  if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
@@ -4058,13 +4526,18 @@ function uniquify(value, row) {
4058
4526
  if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
4059
4527
  return `${value} ${row}`;
4060
4528
  }
4529
+ function digitalish(resource) {
4530
+ return Object.entries(resource.fields).some(
4531
+ ([key, def]) => def.kind === "enum" && /kind|type/.test(key) && def.options.some((option) => /digital|download|licence|license/i.test(option))
4532
+ );
4533
+ }
4061
4534
  function seedColumns(resource) {
4062
- return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4535
+ return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
4063
4536
  }
4064
4537
  function* seedRows(resource, count, options2 = {}) {
4065
4538
  const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
4066
4539
  const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
4067
- const fields = storedFields3(resource);
4540
+ const fields = storedFields4(resource);
4068
4541
  const now = options2.now ?? Date.now();
4069
4542
  const start = options2.startIndex ?? 0;
4070
4543
  for (let i = 0; i < count; i++) {
@@ -4073,7 +4546,7 @@ function* seedRows(resource, count, options2 = {}) {
4073
4546
  for (const [key, def] of fields) {
4074
4547
  let value = fieldValue(key, def, fake, resource, parentIds, number);
4075
4548
  if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
4076
- row[columnName2(key)] = value;
4549
+ row[columnName3(key)] = value;
4077
4550
  }
4078
4551
  const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
4079
4552
  row.created_at = created;
@@ -4082,43 +4555,6 @@ function* seedRows(resource, count, options2 = {}) {
4082
4555
  }
4083
4556
  }
4084
4557
 
4085
- // src/terminal.ts
4086
- var isTTY = () => Boolean(process.stdout.isTTY);
4087
- function formatDuration(ms) {
4088
- const seconds = Math.round(ms / 1e3);
4089
- if (seconds < 60) return `${seconds}s`;
4090
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
4091
- return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
4092
- }
4093
- var formatCount = (value) => value.toLocaleString();
4094
- function formatRate(count, ms, unit = "rows") {
4095
- if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
4096
- return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
4097
- }
4098
- function progressLine(everyMs = 120) {
4099
- if (!isTTY()) return { update: () => {
4100
- }, done: () => {
4101
- } };
4102
- let last = 0;
4103
- let width = 0;
4104
- const write = (message) => {
4105
- process.stdout.write(`\r${message.padEnd(width)}`);
4106
- width = Math.max(width, message.length);
4107
- };
4108
- return {
4109
- update(message) {
4110
- const now = Date.now();
4111
- if (now - last < everyMs) return;
4112
- last = now;
4113
- write(message);
4114
- },
4115
- done() {
4116
- if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
4117
- width = 0;
4118
- }
4119
- };
4120
- }
4121
-
4122
4558
  // src/commands/seed-resource.ts
4123
4559
  async function countRows(db, table) {
4124
4560
  const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
@@ -4143,7 +4579,7 @@ function pickResource(resources, name) {
4143
4579
  }
4144
4580
  return match;
4145
4581
  }
4146
- function pickDatabase(databases, wanted) {
4582
+ function pickDatabase2(databases, wanted) {
4147
4583
  if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4148
4584
  if (wanted) {
4149
4585
  const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
@@ -4172,13 +4608,13 @@ async function seedResourceCommand(name, options2 = {}) {
4172
4608
  ...options2,
4173
4609
  onProgress: (written, total) => {
4174
4610
  const elapsed = Date.now() - started;
4175
- progress.update(` ${pc18.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc18.dim(formatRate(written, elapsed))}`);
4611
+ progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
4176
4612
  }
4177
4613
  });
4178
4614
  progress.done();
4179
4615
  const { rows, ms, where } = result;
4180
4616
  console.log(
4181
- `${pc18.green("\u2714")} Seeded ${pc18.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc18.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4617
+ `${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4182
4618
  );
4183
4619
  return result;
4184
4620
  } catch (error) {
@@ -4189,6 +4625,11 @@ async function seedResourceCommand(name, options2 = {}) {
4189
4625
  async function seedResource(name, options2 = {}) {
4190
4626
  const log2 = options2.log ?? ((message) => console.log(message));
4191
4627
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4628
+ if (readStack(appRoot) === "next") {
4629
+ throw new Error(
4630
+ "flare seed:resource is Cloudflare-only so far. On this stack, write a seed with `flare seed:make <name> --resource " + name + "` \u2014 it comes with rows already filled in \u2014 and run it with `flare seed`."
4631
+ );
4632
+ }
4192
4633
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4193
4634
  const resource = pickResource(resources, name);
4194
4635
  const total = parseCount(options2.count);
@@ -4202,7 +4643,7 @@ async function seedResource(name, options2 = {}) {
4202
4643
  let startIndex = 0;
4203
4644
  const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
4204
4645
  if (options2.remote) {
4205
- const db = pickDatabase(readD1Databases(appRoot), options2.database);
4646
+ const db = pickDatabase2(readD1Databases(appRoot), options2.database);
4206
4647
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4207
4648
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4208
4649
  const query = async (sql2) => {
@@ -4221,8 +4662,8 @@ ${result.output.trim()}`);
4221
4662
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4222
4663
  startIndex = counted?.total ?? 0;
4223
4664
  }
4224
- const dir = mkdtempSync2(join22(tmpdir2(), "flare-seed-"));
4225
- const file = join22(dir, `${resource.table}.sql`);
4665
+ const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4666
+ const file = join24(dir, `${resource.table}.sql`);
4226
4667
  try {
4227
4668
  const statements = [];
4228
4669
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4232,13 +4673,13 @@ ${result.output.trim()}`);
4232
4673
  written += statement.rows;
4233
4674
  options2.onProgress?.(written, total);
4234
4675
  }
4235
- writeFileSync15(file, statements.join("\n"));
4236
- log2(`${pc18.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc18.bold(db.database_name)}`);
4676
+ writeFileSync17(file, statements.join("\n"));
4677
+ log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4237
4678
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4238
4679
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4239
4680
  ${result.output.trim()}`);
4240
4681
  } finally {
4241
- rmSync3(dir, { recursive: true, force: true });
4682
+ rmSync6(dir, { recursive: true, force: true });
4242
4683
  }
4243
4684
  return { resource, rows: total, ms: Date.now() - started, where };
4244
4685
  }
@@ -4265,17 +4706,17 @@ ${result.output.trim()}`);
4265
4706
 
4266
4707
  // src/commands/billing-sync.ts
4267
4708
  import { createRequire as createRequire2 } from "module";
4268
- import { existsSync as existsSync22, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4269
- import { tmpdir as tmpdir3 } from "os";
4270
- import { join as join23 } from "path";
4709
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
4710
+ import { tmpdir as tmpdir4 } from "os";
4711
+ import { join as join25 } from "path";
4271
4712
  import { pathToFileURL as pathToFileURL2 } from "url";
4272
4713
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4273
- import pc19 from "picocolors";
4714
+ import pc20 from "picocolors";
4274
4715
  function stripeKeys(appRoot) {
4275
4716
  const map = {};
4276
- const path = join23(appRoot, ".dev.vars");
4277
- if (existsSync22(path)) {
4278
- for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
4717
+ const path = join25(appRoot, ".dev.vars");
4718
+ if (existsSync23(path)) {
4719
+ for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
4279
4720
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4280
4721
  if (match) map[match[1]] = match[2].trim();
4281
4722
  }
@@ -4358,10 +4799,10 @@ async function syncPlans(options2 = {}) {
4358
4799
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4359
4800
  const db = readD1Databases(appRoot)[0];
4360
4801
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4361
- const appRequire = createRequire2(join23(appRoot, "package.json"));
4802
+ const appRequire = createRequire2(join25(appRoot, "package.json"));
4362
4803
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4363
4804
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4364
- const appName = JSON.parse(readFileSync13(join23(appRoot, "package.json"), "utf8")).name ?? "app";
4805
+ const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
4365
4806
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4366
4807
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4367
4808
  if (products.length === 0) {
@@ -4382,42 +4823,42 @@ async function syncPlans(options2 = {}) {
4382
4823
  ${existingResult.output.trim()}`);
4383
4824
  }
4384
4825
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4385
- const dir = mkdtempSync3(join23(tmpdir3(), "flare-sync-"));
4826
+ const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
4386
4827
  try {
4387
- const file = join23(dir, "sync-plans.sql");
4388
- writeFileSync16(file, renderSyncSql(rows));
4828
+ const file = join25(dir, "sync-plans.sql");
4829
+ writeFileSync18(file, renderSyncSql(rows));
4389
4830
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4390
4831
  if (result.code !== 0) throw new Error(`Could not write plans:
4391
4832
  ${result.output.trim()}`);
4392
4833
  } finally {
4393
- rmSync4(dir, { recursive: true, force: true });
4834
+ rmSync7(dir, { recursive: true, force: true });
4394
4835
  }
4395
4836
  let created = 0;
4396
4837
  for (const row of rows) {
4397
4838
  const isNew = !known.has(row.stripePriceId);
4398
4839
  if (isNew) created++;
4399
4840
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
4400
- log2(`${(isNew ? pc19.green("create") : pc19.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc19.dim(price)}`);
4841
+ log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
4401
4842
  }
4402
- for (const name of skipped) log2(pc19.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4843
+ for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4403
4844
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
4404
- if (configuration) log2(`${pc19.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4405
- log2(pc19.green(`
4845
+ if (configuration) log2(`${pc20.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4846
+ log2(pc20.green(`
4406
4847
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
4407
4848
  return { created, updated: rows.length - created, skipped };
4408
4849
  }
4409
4850
 
4410
4851
  // src/commands/sync.ts
4411
- import { existsSync as existsSync23 } from "fs";
4412
- import { join as join24 } from "path";
4413
- import pc20 from "picocolors";
4852
+ import { existsSync as existsSync24 } from "fs";
4853
+ import { join as join26 } from "path";
4854
+ import pc21 from "picocolors";
4414
4855
  async function syncTypes(options2 = {}) {
4415
4856
  const log2 = options2.log ?? ((message) => console.log(message));
4416
4857
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4417
4858
  const all = await loadResources(appRoot);
4418
4859
  const policies = await loadPolicies(appRoot);
4419
- const tracksPolicies = policies.length > 0 || existsSync23(join24(appRoot, "policies/index.ts"));
4420
- const plan = planFiles(all, tracksPolicies ? policies : void 0);
4860
+ const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
4861
+ const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4421
4862
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4422
4863
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
4423
4864
  logResults(results, log2);
@@ -4428,42 +4869,42 @@ async function syncTypes(options2 = {}) {
4428
4869
  if (drift.length) {
4429
4870
  log2(
4430
4871
  `
4431
- ${pc20.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4872
+ ${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4432
4873
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
4433
4874
  );
4434
4875
  }
4435
4876
  if (unmarked.length) {
4436
4877
  log2(`
4437
- ${pc20.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4878
+ ${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4438
4879
  }
4439
4880
  if (orphans.length) {
4440
4881
  const resources = [...new Set(orphans.map((o) => o.resource))];
4441
4882
  log2(`
4442
- ${pc20.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4883
+ ${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4443
4884
  for (const orphan of orphans) log2(` ${orphan.path}`);
4444
4885
  log2(
4445
- `If the resource was deleted, remove them with ${pc20.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4886
+ `If the resource was deleted, remove them with ${pc21.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4446
4887
  );
4447
4888
  }
4448
4889
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
4449
4890
  if (tablesChanged && !options2.check) {
4450
4891
  log2(`
4451
- ${pc20.cyan("Tables changed.")} Create a migration with ${pc20.bold("flare gen migration <name> --from-schema")}, then ${pc20.bold("flare migrate")}.`);
4892
+ ${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
4452
4893
  }
4453
4894
  const problems = drift.length + unmarked.length + orphans.length;
4454
4895
  if (options2.check) {
4455
4896
  const stale = changed.length;
4456
- if (stale === 0 && problems === 0) log2(pc20.green("Generated files are in sync with the resource descriptors."));
4897
+ if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
4457
4898
  else log2(`
4458
- ${pc20.red("Out of sync:")} ${stale} file(s) would change. Run ${pc20.bold("flare sync-types")}.`);
4899
+ ${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
4459
4900
  return stale + problems > 0 ? 1 : 0;
4460
4901
  }
4461
- if (changed.length === 0 && problems === 0) log2(pc20.green("Everything is in sync."));
4902
+ if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
4462
4903
  return problems > 0 ? 1 : 0;
4463
4904
  }
4464
4905
 
4465
4906
  // src/commands/user-role.ts
4466
- import pc21 from "picocolors";
4907
+ import pc22 from "picocolors";
4467
4908
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
4468
4909
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
4469
4910
  function userRoleSql(email, role) {
@@ -4477,8 +4918,8 @@ async function setUserRole(email, role, options2 = {}) {
4477
4918
  const sql2 = userRoleSql(email, role);
4478
4919
  const roles = await listRoles(options2);
4479
4920
  if (roles && !roles.includes(role)) {
4480
- log2(pc21.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4481
- log2(pc21.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4921
+ log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4922
+ log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4482
4923
  return 1;
4483
4924
  }
4484
4925
  const db = readD1Databases(appRoot)[0];
@@ -4492,31 +4933,31 @@ ${result.output.trim()}`);
4492
4933
  const [first] = extractJson(result.output);
4493
4934
  const updated = first?.results ?? [];
4494
4935
  if (updated.length === 0) {
4495
- log2(pc21.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4936
+ log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4496
4937
  return 1;
4497
4938
  }
4498
- log2(pc21.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4939
+ log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4499
4940
  return 0;
4500
4941
  }
4501
4942
 
4502
4943
  // src/utils/install.ts
4503
4944
  import { spawn as spawn5 } from "child_process";
4504
- import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
4505
- import { join as join25 } from "path";
4945
+ import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
4946
+ import { join as join27 } from "path";
4506
4947
  function expectedPackages(dir) {
4507
- const npmLock = join25(dir, "package-lock.json");
4508
- if (existsSync24(npmLock)) {
4948
+ const npmLock = join27(dir, "package-lock.json");
4949
+ if (existsSync25(npmLock)) {
4509
4950
  try {
4510
- const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
4951
+ const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
4511
4952
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4512
4953
  } catch {
4513
4954
  return 0;
4514
4955
  }
4515
4956
  }
4516
- const pnpmLock = join25(dir, "pnpm-lock.yaml");
4517
- if (existsSync24(pnpmLock)) {
4957
+ const pnpmLock = join27(dir, "pnpm-lock.yaml");
4958
+ if (existsSync25(pnpmLock)) {
4518
4959
  try {
4519
- const text = readFileSync14(pnpmLock, "utf8");
4960
+ const text = readFileSync15(pnpmLock, "utf8");
4520
4961
  const packages = text.slice(text.indexOf("\npackages:"));
4521
4962
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4522
4963
  } catch {
@@ -4526,15 +4967,15 @@ function expectedPackages(dir) {
4526
4967
  return 0;
4527
4968
  }
4528
4969
  function installedPackages(dir) {
4529
- const root = join25(dir, "node_modules");
4530
- if (!existsSync24(root)) return 0;
4970
+ const root = join27(dir, "node_modules");
4971
+ if (!existsSync25(root)) return 0;
4531
4972
  let total = 0;
4532
4973
  try {
4533
4974
  for (const entry of readdirSync9(root, { withFileTypes: true })) {
4534
4975
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4535
4976
  if (entry.name.startsWith("@")) {
4536
4977
  try {
4537
- total += readdirSync9(join25(root, entry.name)).length;
4978
+ total += readdirSync9(join27(root, entry.name)).length;
4538
4979
  } catch {
4539
4980
  }
4540
4981
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -4578,31 +5019,33 @@ function install(packageManager, dir, onProgress) {
4578
5019
  // src/cli.ts
4579
5020
  function createCli() {
4580
5021
  const cli = cac("flare");
4581
- cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(async (dir, options2) => {
4582
- const wanted = !options2.skipInstall;
4583
- let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
4584
- if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
4585
- const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
4586
- let installed = false;
4587
- if (wanted) {
4588
- const spin = prompts3.spinner({ indicator: "timer" });
4589
- spin.start(`Installing dependencies with ${result.packageManager}`);
4590
- const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
4591
- spin.message(
4592
- percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
4593
- );
4594
- });
4595
- installed = outcome.code === 0;
4596
- if (installed) {
4597
- spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc22.dim(`(${formatDuration(outcome.ms)})`)}`);
4598
- } else {
4599
- spin.error(`${result.packageManager} install failed`);
4600
- process.stderr.write(outcome.output);
4601
- process.exitCode = 1;
5022
+ cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
5023
+ async (dir, options2) => {
5024
+ const wanted = !options2.skipInstall;
5025
+ let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
5026
+ if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
5027
+ const result = createApp(dir, { pm: options2.pm, stack: options2.stack, install: false, ...answers, log: (message) => prompts3.log.message(message) });
5028
+ let installed = false;
5029
+ if (wanted) {
5030
+ const spin = prompts3.spinner({ indicator: "timer" });
5031
+ spin.start(`Installing dependencies with ${result.packageManager}`);
5032
+ const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
5033
+ spin.message(
5034
+ percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
5035
+ );
5036
+ });
5037
+ installed = outcome.code === 0;
5038
+ if (installed) {
5039
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
5040
+ } else {
5041
+ spin.error(`${result.packageManager} install failed`);
5042
+ process.stderr.write(outcome.output);
5043
+ process.exitCode = 1;
5044
+ }
4602
5045
  }
5046
+ printNextSteps(result, installed);
4603
5047
  }
4604
- printNextSteps(result, installed);
4605
- });
5048
+ );
4606
5049
  cli.command("gen <generator> [name] [second]", "Generate code. Generators: resource, endpoint, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
4607
5050
  async (generator, name, second, options2) => {
4608
5051
  if (generator === "billing")
@@ -4643,6 +5086,9 @@ function createCli() {
4643
5086
  cli.command("seed:resource <resource> [count]", "Fill a resource's table with sample rows built from its descriptor").option("--count <rows>", "How many rows: 1000, 25k, 1m (default: 25)").option("--remote", "Seed the deployed database instead of the local one (needs --yes)").option("--truncate", "Delete the table's rows first").option("--seed <number>", "Same number, same rows").option("--env <name>", "Wrangler environment").option("--database <binding>", "D1 binding, when the app has several").option("-y, --yes", "Confirm writing to the remote database").example("flare seed:resource Contact --count 1000").example("flare seed:resource Contact 1m # a million rows").example("flare seed:resource Contact 5k --remote --yes").action(async (resource, count, options2) => {
4644
5087
  await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
4645
5088
  });
5089
+ cli.command("db:push [...tables]", "Copy rows from the local database to the deployed one (seeded catalogues, reference data)").option("--truncate", "Delete the remote rows of those tables first").option("--database <binding>", "D1 binding, when the app has several").option("--env <name>", "Wrangler environment").option("-y, --yes", "Confirm writing to the deployed database").example("flare db:push products categories --yes").example("flare db:push plans --truncate --yes").action(async (tables, options2) => {
5090
+ await dbPush(tables, options2);
5091
+ });
4646
5092
  cli.command("seed:make <name>", "Create seeds/<name>.seed.ts (with example rows for a matching resource)").option("--resource <name>", "Resource to write example rows for").action(async (name, options2) => {
4647
5093
  await makeSeed(name, options2);
4648
5094
  });
@@ -4688,6 +5134,6 @@ try {
4688
5134
  }
4689
5135
  }
4690
5136
  } catch (error) {
4691
- console.error(pc23.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
5137
+ console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4692
5138
  process.exitCode = 1;
4693
5139
  }