@flaredev/cli 0.4.1 → 0.5.1

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 (48) hide show
  1. package/dist/index.js +698 -341
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  4. package/templates/app/app/dashboard/page.tsx +4 -7
  5. package/templates/app/app/page.tsx +10 -0
  6. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  7. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  8. package/templates/app/components/dashboard/record-actions.tsx +7 -7
  9. package/templates/app/components/dashboard/record-detail.tsx +1 -1
  10. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  11. package/templates/app/components/dashboard/resource-form.tsx +129 -33
  12. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  13. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  14. package/templates/app/components/dashboard/resource-table.tsx +4 -4
  15. package/templates/app/components/dashboard/row-actions.tsx +3 -3
  16. package/templates/app/lib/costs.ts +138 -0
  17. package/templates/app/lib/dashboard-nav.ts +5 -1
  18. package/templates/app/lib/dashboard.ts +66 -61
  19. package/templates/app/lib/number.ts +33 -0
  20. package/templates/app/lib/site.ts +56 -56
  21. package/templates/app/lib/suggest.ts +35 -0
  22. package/templates/app/lib/usage.ts +32 -0
  23. package/templates/next/.env.example +21 -0
  24. package/templates/next/README.md +16 -0
  25. package/templates/next/app/api/health/route.ts +7 -0
  26. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  27. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  28. package/templates/next/app/dashboard/actions.ts +277 -0
  29. package/templates/next/app/dashboard/layout.tsx +64 -0
  30. package/templates/next/app/dashboard/page.tsx +127 -0
  31. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  32. package/templates/next/lib/analytics.ts +71 -0
  33. package/templates/next/lib/audit.ts +80 -0
  34. package/templates/next/lib/auth-emails.ts +92 -0
  35. package/templates/next/lib/auth.ts +156 -0
  36. package/templates/next/lib/dashboard.ts +161 -0
  37. package/templates/next/lib/db.ts +24 -0
  38. package/templates/next/lib/mail.ts +35 -0
  39. package/templates/next/lib/realtime.ts +23 -0
  40. package/templates/next/lib/redis.ts +17 -0
  41. package/templates/next/lib/storage.ts +57 -0
  42. package/templates/next/lib/theme.ts +83 -0
  43. package/templates/next/lib/views.ts +23 -0
  44. package/templates/next/next.config.ts +13 -0
  45. package/templates/next/prisma/schema/base.prisma +162 -0
  46. package/templates/next/prisma.config.ts +15 -0
  47. package/templates/next/tsconfig.json +24 -0
  48. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
package/dist/index.js CHANGED
@@ -9,8 +9,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";
@@ -186,42 +186,94 @@ var APP_DEV_DEPENDENCIES = {
186
186
  vite: "^8.3.0",
187
187
  wrangler: "^4.132.0"
188
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
+ var STACK_LABELS = {
228
+ cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
229
+ next: "Next.js on Vercel, Neon Postgres and Prisma \u2014 real Postgres, Node runtime"
230
+ };
231
+ function readStack(appRoot) {
232
+ const file = join(appRoot, "package.json");
233
+ if (!existsSync(file)) return "cloudflare";
234
+ try {
235
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
236
+ return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
237
+ } catch {
238
+ return "cloudflare";
239
+ }
240
+ }
189
241
 
190
242
  // src/utils/fs.ts
191
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
192
- import { dirname, join, resolve } from "path";
243
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
244
+ import { dirname, join as join2, resolve } from "path";
193
245
  import { fileURLToPath } from "url";
194
246
  var RENAMES = { _gitignore: ".gitignore" };
195
247
  function copyTemplate(from, to, tokens) {
196
248
  mkdirSync(to, { recursive: true });
197
249
  let files = 0;
198
250
  for (const entry of readdirSync(from)) {
199
- const src = join(from, entry);
200
- const dest = join(to, RENAMES[entry] ?? entry);
251
+ const src = join2(from, entry);
252
+ const dest = join2(to, RENAMES[entry] ?? entry);
201
253
  if (statSync(src).isDirectory()) {
202
254
  files += copyTemplate(src, dest, tokens);
203
255
  continue;
204
256
  }
205
- const content = readFileSync(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
206
- writeFileSync(dest, content);
257
+ const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
258
+ writeFileSync2(dest, content);
207
259
  files++;
208
260
  }
209
261
  return files;
210
262
  }
211
263
  function writeJson(path, value) {
212
264
  mkdirSync(dirname(path), { recursive: true });
213
- writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
265
+ writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
214
266
  }
215
267
  function findUp(file, start) {
216
268
  let dir = resolve(start);
217
269
  while (true) {
218
- if (existsSync(join(dir, file))) return dir;
270
+ if (existsSync2(join2(dir, file))) return dir;
219
271
  const parent = dirname(dir);
220
272
  if (parent === dir) return void 0;
221
273
  dir = parent;
222
274
  }
223
275
  }
224
- var templatesDir = join(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
276
+ var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
225
277
 
226
278
  // src/utils/pm.ts
227
279
  import { spawnSync } from "child_process";
@@ -260,7 +312,7 @@ function toAppName(dir) {
260
312
  function flarePackageSpec(pkg, appDir, packageManager) {
261
313
  const repoPackage = resolve2(templatesDir, "../..", pkg);
262
314
  const repoRoot = resolve2(repoPackage, "../..");
263
- const fromCheckout = existsSync2(join2(repoPackage, "package.json")) && existsSync2(join2(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
315
+ const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
264
316
  if (!fromCheckout) return `^${FLARE_VERSION}`;
265
317
  const rel = relative(repoRoot, appDir);
266
318
  if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
@@ -270,32 +322,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
270
322
  function createApp(target, options2 = {}) {
271
323
  const dir = resolve2(target);
272
324
  const name = toAppName(dir);
273
- if (existsSync2(dir) && readdirSync2(dir).length > 0) {
325
+ if (existsSync3(dir) && readdirSync2(dir).length > 0) {
274
326
  throw new Error(`Directory ${dir} already exists and is not empty.`);
275
327
  }
276
328
  if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
277
329
  throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
278
330
  }
279
331
  const packageManager = options2.pm ?? detectPackageManager();
332
+ if (options2.stack !== void 0 && !isStack(options2.stack)) {
333
+ throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
334
+ }
335
+ const stack = options2.stack ?? "cloudflare";
280
336
  const authProviders = parseAuthProviders(options2.authProviders);
281
337
  const authMethods = parseAuthMethods(options2.auth);
282
338
  const theme = parseTheme(options2.theme);
283
339
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
284
340
  const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
285
- const files = copyTemplate(join2(templatesDir, "app"), dir, {
341
+ const files = copyTemplate(join3(templatesDir, "app"), dir, {
286
342
  APP_NAME: name,
287
343
  COMPAT_DATE: compatibilityDate,
288
344
  PM: packageManager,
289
345
  THEME: theme
290
346
  });
291
- writeFileSync2(join2(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
292
- appendFileSync(join2(dir, ".dev.vars.example"), devVarsExampleEntries(authProviders));
293
- writeJson(join2(dir, "package.json"), {
347
+ if (stack === "next") {
348
+ copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
349
+ for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
350
+ }
351
+ writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
352
+ appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
353
+ const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
354
+ const uiOnly = Object.fromEntries(
355
+ Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
356
+ );
357
+ writeJson(join3(dir, "package.json"), {
294
358
  name,
295
359
  version: "0.1.0",
296
360
  private: true,
297
361
  type: "module",
298
- scripts: {
362
+ // Which stack this app targets. `flare gen` reads it; don't change it by hand.
363
+ flare: { stack },
364
+ scripts: stack === "next" ? {
365
+ dev: "next dev",
366
+ build: "prisma generate && next build",
367
+ start: "next start",
368
+ deploy: "vercel deploy --prod",
369
+ "db:generate": "prisma generate",
370
+ "db:migrate": "prisma migrate dev",
371
+ "db:studio": "prisma studio"
372
+ } : {
299
373
  dev: "flare dev",
300
374
  build: "flare build",
301
375
  start: "flare start",
@@ -304,18 +378,25 @@ function createApp(target, options2 = {}) {
304
378
  "db:generate": "drizzle-kit generate",
305
379
  "db:migrate:local": "wrangler d1 migrations apply DB --local"
306
380
  },
307
- dependencies: { "@flaredev/core": flarePackageSpec("core", dir, packageManager), ...APP_DEPENDENCIES },
308
- devDependencies: { "@flaredev/cli": flarePackageSpec("cli", dir, packageManager), ...APP_DEV_DEPENDENCIES }
381
+ dependencies: {
382
+ "@flaredev/core": flarePackageSpec("core", dir, packageManager),
383
+ ...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
384
+ },
385
+ devDependencies: {
386
+ "@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
387
+ ...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
388
+ }
309
389
  });
310
- writeFileSync2(
311
- join2(dir, ".dev.vars"),
390
+ writeFileSync3(
391
+ join3(dir, stack === "next" ? ".env" : ".dev.vars"),
312
392
  `BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
313
393
  RESEND_API_KEY=
314
394
  MAIL_FROM=
315
395
  ${devVarsEntries(authProviders)}`
316
396
  );
317
397
  if (packageManager === "pnpm" && !inWorkspace) {
318
- writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
398
+ 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";
399
+ writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
319
400
  }
320
401
  const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
321
402
  if (pinned) copyLockfile(packageManager, dir);
@@ -326,9 +407,9 @@ ${devVarsEntries(authProviders)}`
326
407
  function copyLockfile(packageManager, dir) {
327
408
  const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
328
409
  if (!file) return false;
329
- const source = join2(templatesDir, "locks", file);
330
- if (!existsSync2(source)) return false;
331
- copyFileSync(source, join2(dir, file));
410
+ const source = join3(templatesDir, "locks", file);
411
+ if (!existsSync3(source)) return false;
412
+ copyFileSync(source, join3(dir, file));
332
413
  return true;
333
414
  }
334
415
  function printNextSteps(result, installed) {
@@ -348,8 +429,8 @@ function printNextSteps(result, installed) {
348
429
  }
349
430
 
350
431
  // src/commands/gen-billing.ts
351
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
352
- import { dirname as dirname6, join as join11 } from "path";
432
+ import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
433
+ import { dirname as dirname6, join as join12 } from "path";
353
434
  import pc6 from "picocolors";
354
435
 
355
436
  // src/generator/grammar.ts
@@ -513,7 +594,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
513
594
 
514
595
  // src/generator/markers.ts
515
596
  import { createHash } from "crypto";
516
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
597
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
517
598
  import { dirname as dirname3 } from "path";
518
599
  var START = "// generated:start";
519
600
  var END = "// generated:end";
@@ -563,8 +644,8 @@ ${indentBlock(body, indent)}${indent}${END}
563
644
  ${after}`;
564
645
  }
565
646
  function inspectGenerated(path, content) {
566
- if (!existsSync3(path)) return "missing";
567
- const parts = splitMarkers(readFileSync2(path, "utf8"));
647
+ if (!existsSync4(path)) return "missing";
648
+ const parts = splitMarkers(readFileSync3(path, "utf8"));
568
649
  if (!parts) return "unmarked";
569
650
  if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
570
651
  if (normalize(parts.block) !== normalize(content)) return "outdated";
@@ -580,18 +661,18 @@ var DriftError = class extends Error {
580
661
  };
581
662
  function writeGenerated(path, content, options2 = {}) {
582
663
  const hash = hashBlock(content);
583
- if (!existsSync3(path)) {
664
+ if (!existsSync4(path)) {
584
665
  mkdirSync2(dirname3(path), { recursive: true });
585
- writeFileSync3(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
666
+ writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
586
667
  return "create";
587
668
  }
588
- const source = readFileSync2(path, "utf8");
669
+ const source = readFileSync3(path, "utf8");
589
670
  const parts = splitMarkers(source);
590
671
  if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
591
672
  if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
592
673
  const next = joinMarkers({ ...parts, block: content, hash });
593
674
  if (next === source) return "identical";
594
- writeFileSync3(path, next);
675
+ writeFileSync4(path, next);
595
676
  return "update";
596
677
  }
597
678
 
@@ -1346,17 +1427,17 @@ var billingDevVarsExampleEntries = () => [
1346
1427
  ].join("\n");
1347
1428
 
1348
1429
  // src/generator/load.ts
1349
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
1350
- import { join as join3, resolve as resolve3 } from "path";
1430
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
1431
+ import { join as join4, resolve as resolve3 } from "path";
1351
1432
  import { createJiti } from "jiti";
1352
1433
  async function loadResources(root) {
1353
1434
  const appRoot = resolve3(root);
1354
- const dir = join3(appRoot, "resources");
1355
- if (!existsSync4(dir)) return [];
1356
- const jiti = createJiti(join3(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
1435
+ const dir = join4(appRoot, "resources");
1436
+ if (!existsSync5(dir)) return [];
1437
+ const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
1357
1438
  const loaded = [];
1358
1439
  for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
1359
- const path = join3(dir, file);
1440
+ const path = join4(dir, file);
1360
1441
  const mod = await jiti.import(path);
1361
1442
  const resource = mod.default;
1362
1443
  if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
@@ -1374,20 +1455,20 @@ async function loadResources(root) {
1374
1455
 
1375
1456
  // src/generator/migrations.ts
1376
1457
  import { spawn as spawn4 } from "child_process";
1377
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1378
- import { join as join8 } from "path";
1458
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1459
+ import { join as join9 } from "path";
1379
1460
 
1380
1461
  // src/commands/run.ts
1381
1462
  import { spawn as spawn3 } from "child_process";
1382
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1383
- import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
1463
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1464
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
1384
1465
  import pc4 from "picocolors";
1385
1466
 
1386
1467
  // src/tunnel.ts
1387
1468
  import { spawn, spawnSync as spawnSync2 } from "child_process";
1388
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
1469
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1389
1470
  import { homedir } from "os";
1390
- import { join as join4 } from "path";
1471
+ import { join as join5 } from "path";
1391
1472
  import pc2 from "picocolors";
1392
1473
  var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
1393
1474
  var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
@@ -1412,7 +1493,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
1412
1493
  throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
1413
1494
  }
1414
1495
  var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
1415
- var cacheDir = () => join4(homedir(), ".flare", "bin");
1496
+ var cacheDir = () => join5(homedir(), ".flare", "bin");
1416
1497
  function onPath() {
1417
1498
  const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1418
1499
  return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
@@ -1421,19 +1502,19 @@ async function download(log2) {
1421
1502
  const asset = releaseAsset();
1422
1503
  const dir = cacheDir();
1423
1504
  mkdirSync3(dir, { recursive: true });
1424
- const target = join4(dir, binName);
1505
+ const target = join5(dir, binName);
1425
1506
  log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1426
1507
  const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1427
1508
  if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1428
1509
  const bytes = Buffer.from(await response.arrayBuffer());
1429
1510
  const partial = `${target}.download`;
1430
1511
  if (asset.endsWith(".tgz")) {
1431
- writeFileSync4(`${partial}.tgz`, bytes);
1512
+ writeFileSync5(`${partial}.tgz`, bytes);
1432
1513
  const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
1433
- rmSync(`${partial}.tgz`, { force: true });
1514
+ rmSync2(`${partial}.tgz`, { force: true });
1434
1515
  if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
1435
1516
  } else {
1436
- writeFileSync4(partial, bytes);
1517
+ writeFileSync5(partial, bytes);
1437
1518
  renameSync(partial, target);
1438
1519
  }
1439
1520
  if (process.platform !== "win32") chmodSync(target, 493);
@@ -1442,8 +1523,8 @@ async function download(log2) {
1442
1523
  async function ensureCloudflared(log2 = console.log) {
1443
1524
  const found = onPath();
1444
1525
  if (found) return found;
1445
- const cached = join4(cacheDir(), binName);
1446
- if (existsSync5(cached)) return cached;
1526
+ const cached = join5(cacheDir(), binName);
1527
+ if (existsSync6(cached)) return cached;
1447
1528
  return download(log2);
1448
1529
  }
1449
1530
  async function openTunnel(localUrl, options2 = {}) {
@@ -1499,16 +1580,16 @@ function tunnelBanner(publicUrl, localUrl) {
1499
1580
 
1500
1581
  // src/commands/deploy.ts
1501
1582
  import { randomBytes as randomBytes2 } from "crypto";
1502
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1503
- import { join as join6 } from "path";
1583
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1584
+ import { join as join7 } from "path";
1504
1585
  import { createZoneClient } from "@flaredev/core/security";
1505
1586
  import { createJiti as createJiti2 } from "jiti";
1506
1587
  import pc3 from "picocolors";
1507
1588
 
1508
1589
  // src/utils/wrangler.ts
1509
1590
  import { spawn as spawn2 } from "child_process";
1510
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1511
- import { join as join5 } from "path";
1591
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1592
+ import { join as join6 } from "path";
1512
1593
  import { parse as parseJsonc } from "jsonc-parser";
1513
1594
  function runWrangler(wranglerBin, args, cwd, options2 = {}) {
1514
1595
  return new Promise((resolve6, reject) => {
@@ -1533,9 +1614,9 @@ ${output.trim()}`);
1533
1614
  return JSON.parse(output.slice(start, end + 1));
1534
1615
  }
1535
1616
  function readD1Databases(appRoot) {
1536
- const path = join5(appRoot, "wrangler.jsonc");
1537
- if (!existsSync6(path)) return [];
1538
- const config = parseJsonc(readFileSync3(path, "utf8"));
1617
+ const path = join6(appRoot, "wrangler.jsonc");
1618
+ if (!existsSync7(path)) return [];
1619
+ const config = parseJsonc(readFileSync4(path, "utf8"));
1539
1620
  return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
1540
1621
  }
1541
1622
 
@@ -1575,15 +1656,15 @@ function declaredSecretNames(devVarsExample) {
1575
1656
  return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
1576
1657
  }
1577
1658
  async function loadSecurityConfig(appRoot) {
1578
- const path = join6(appRoot, "security.config.ts");
1579
- if (!existsSync7(path)) return null;
1580
- const jiti = createJiti2(join6(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1659
+ const path = join7(appRoot, "security.config.ts");
1660
+ if (!existsSync8(path)) return null;
1661
+ const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1581
1662
  const mod = await jiti.import(path);
1582
1663
  if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
1583
1664
  return mod.default;
1584
1665
  }
1585
1666
  async function provisionZoneSecurity(appRoot, config, credentials, options2) {
1586
- const app = JSON.parse(readFileSync4(join6(appRoot, "package.json"), "utf8")).name ?? "app";
1667
+ const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
1587
1668
  const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
1588
1669
  if (config.zone?.rateLimits) {
1589
1670
  const result = await zone.syncRateLimits(config.zone.rateLimits);
@@ -1603,7 +1684,7 @@ async function runDeploy(args, context3) {
1603
1684
  const envArgs = plan.env ? ["--env", plan.env] : [];
1604
1685
  const step = (message) => log2(`
1605
1686
  ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1606
- const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1687
+ const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
1607
1688
  const migrateAfterDeploy = [];
1608
1689
  const migrate2 = async (db) => {
1609
1690
  step(`Applying migrations to ${db.database_name} (remote)`);
@@ -1662,8 +1743,8 @@ ${put.output.trim()}`);
1662
1743
  existing.add(name);
1663
1744
  }
1664
1745
  }
1665
- const examplePath = join6(appRoot, ".dev.vars.example");
1666
- const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1746
+ const examplePath = join7(appRoot, ".dev.vars.example");
1747
+ const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1667
1748
  if (optional.length > 0) {
1668
1749
  step("Optional secrets not set in production (features using them stay off):");
1669
1750
  for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
@@ -1698,25 +1779,30 @@ function findAppRoot(cwd) {
1698
1779
  while (dir) {
1699
1780
  const root = findUp("package.json", dir);
1700
1781
  if (!root) break;
1701
- const pkg = JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
1702
- if (pkg.dependencies?.vinext || pkg.devDependencies?.vinext) return root;
1782
+ const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
1783
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1784
+ if (deps["@flaredev/core"] || deps.vinext) return root;
1703
1785
  const parent = dirname4(root);
1704
1786
  dir = parent === root ? void 0 : parent;
1705
1787
  }
1706
- throw new Error("Not inside a Flare app (no package.json depending on vinext found). Run this from your app directory.");
1788
+ throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
1707
1789
  }
1708
1790
  function resolveBin(appRoot, pkgName, binName2) {
1709
- const pkgDir = findUp(join7("node_modules", pkgName, "package.json"), appRoot);
1791
+ const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
1710
1792
  if (!pkgDir) {
1711
1793
  throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
1712
1794
  }
1713
- const manifestPath = join7(pkgDir, "node_modules", pkgName, "package.json");
1714
- const { bin } = JSON.parse(readFileSync5(manifestPath, "utf8"));
1795
+ const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
1796
+ const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
1715
1797
  const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
1716
1798
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1717
- return join7(dirname4(manifestPath), relativeBin);
1799
+ return join8(dirname4(manifestPath), relativeBin);
1718
1800
  }
1719
1801
  function delegatedArgv(appRoot, command, forwarded) {
1802
+ if (readStack(appRoot) === "next") {
1803
+ const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
1804
+ throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
1805
+ }
1720
1806
  const spec = DELEGATED_COMMANDS[command];
1721
1807
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1722
1808
  }
@@ -1775,7 +1861,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
1775
1861
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1776
1862
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1777
1863
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1778
- if (command === "start" && !wantsHelp && !existsSync8(join7(appRoot, "dist/server/wrangler.json"))) {
1864
+ if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
1779
1865
  console.log("No production build found; running `flare build` first.\n");
1780
1866
  const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1781
1867
  if (code !== 0) return code;
@@ -1825,9 +1911,9 @@ function snapshotColumns(snapshot) {
1825
1911
  }
1826
1912
 
1827
1913
  // src/generator/migrations.ts
1828
- var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1914
+ var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1829
1915
  async function generateSchemaMigration(appRoot, name, options2 = {}) {
1830
- const dir = join8(appRoot, "migrations");
1916
+ const dir = join9(appRoot, "migrations");
1831
1917
  const before = new Set(listSql(dir));
1832
1918
  const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
1833
1919
  const { code, output } = await new Promise((resolve6, reject) => {
@@ -1855,10 +1941,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1855
1941
  for (const file of created) {
1856
1942
  const previous = previousSnapshot(dir, file);
1857
1943
  if (!previous) continue;
1858
- const path = join8(dir, file);
1859
- const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
1944
+ const path = join9(dir, file);
1945
+ const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
1860
1946
  if (changes.length) {
1861
- writeFileSync5(path, sql2);
1947
+ writeFileSync6(path, sql2);
1862
1948
  repairs.push(...changes.map((change) => `${file}: ${change}`));
1863
1949
  }
1864
1950
  }
@@ -1866,32 +1952,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1866
1952
  }
1867
1953
  function previousSnapshot(dir, migrationFile) {
1868
1954
  const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
1869
- const metaDir = join8(dir, "meta");
1870
- if (!prefix || !existsSync9(metaDir)) return void 0;
1955
+ const metaDir = join9(dir, "meta");
1956
+ if (!prefix || !existsSync10(metaDir)) return void 0;
1871
1957
  const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
1872
- return earlier ? JSON.parse(readFileSync6(join8(metaDir, earlier), "utf8")) : void 0;
1958
+ return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
1873
1959
  }
1874
1960
 
1875
1961
  // src/generator/plan.ts
1876
- import { copyFileSync as copyFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1877
- import { dirname as dirname5, join as join10 } from "path";
1962
+ import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
1963
+ import { dirname as dirname5, join as join11 } from "path";
1878
1964
  import pc5 from "picocolors";
1879
1965
 
1880
1966
  // src/generator/policy.ts
1881
- import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1882
- import { join as join9, resolve as resolve5 } from "path";
1967
+ import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
1968
+ import { join as join10, resolve as resolve5 } from "path";
1883
1969
  import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
1884
1970
  import { createJiti as createJiti3 } from "jiti";
1885
1971
  var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
1886
1972
  var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
1887
1973
  async function loadPolicies(root) {
1888
1974
  const appRoot = resolve5(root);
1889
- const dir = join9(appRoot, "policies");
1890
- if (!existsSync10(dir)) return [];
1891
- const jiti = createJiti3(join9(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1975
+ const dir = join10(appRoot, "policies");
1976
+ if (!existsSync11(dir)) return [];
1977
+ const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1892
1978
  const loaded = [];
1893
1979
  for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
1894
- const mod = await jiti.import(join9(dir, file));
1980
+ const mod = await jiti.import(join10(dir, file));
1895
1981
  const policy = mod.default;
1896
1982
  if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
1897
1983
  throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
@@ -1936,15 +2022,136 @@ function renderPolicyRegistry(all) {
1936
2022
  ].join("\n");
1937
2023
  }
1938
2024
 
1939
- // src/generator/render.ts
1940
- import { camelCase as camelCase3, columnName, relationGraph, storedFields } from "@flaredev/core";
1941
- var resourceLocal = (stem) => `${camelCase3(stem)}Resource`;
1942
- var tableExport = (resource) => camelCase3(resource.table);
2025
+ // src/generator/prisma.ts
2026
+ import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
1943
2027
  var q = (value) => JSON.stringify(value);
2028
+ var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
2029
+ var enumMember = (option) => {
2030
+ const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
2031
+ return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
2032
+ };
2033
+ function prismaType(resource, key, def) {
2034
+ switch (def.kind) {
2035
+ case "int":
2036
+ return "Int";
2037
+ case "float":
2038
+ return "Float";
2039
+ case "boolean":
2040
+ return "Boolean";
2041
+ case "date":
2042
+ case "datetime":
2043
+ return "DateTime";
2044
+ case "enum":
2045
+ return enumTypeName(resource.name, key);
2046
+ case "multiselect":
2047
+ return "String[]";
2048
+ default:
2049
+ return "String";
2050
+ }
2051
+ }
2052
+ function attributes(key, def) {
2053
+ const parts = [];
2054
+ const column = columnName(key);
2055
+ if (column !== key) parts.push(`@map(${q(column)})`);
2056
+ if ("unique" in def && def.unique) parts.push("@unique");
2057
+ if (def.kind === "text") parts.push("@db.Text");
2058
+ if (def.kind === "date") parts.push("@db.Date");
2059
+ if ("default" in def && def.default !== void 0) {
2060
+ const value = def.default;
2061
+ if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
2062
+ else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
2063
+ else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
2064
+ }
2065
+ return parts;
2066
+ }
2067
+ var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
2068
+ function renderPrismaEnums(resource) {
2069
+ return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
2070
+ if (def.kind !== "enum") return "";
2071
+ return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
2072
+ }).filter(Boolean);
2073
+ }
2074
+ function backReferences(target, all) {
2075
+ const found = [];
2076
+ for (const { resource: source } of all) {
2077
+ const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
2078
+ const ambiguous = pointing.length > 1;
2079
+ for (const [key, def] of pointing) {
2080
+ if (def.kind !== "belongsTo") continue;
2081
+ const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
2082
+ const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
2083
+ found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
2084
+ }
2085
+ }
2086
+ return found;
2087
+ }
2088
+ function renderPrismaModel(entry, all) {
2089
+ const resource = entry.resource;
2090
+ const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
2091
+ const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
2092
+ const indexes = [];
2093
+ for (const [key, def] of Object.entries(resource.fields)) {
2094
+ if (def.kind === "hasMany") {
2095
+ if (!byName.get(def.target)) {
2096
+ throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2097
+ }
2098
+ continue;
2099
+ }
2100
+ if (def.kind === "belongsTo") {
2101
+ const target = byName.get(def.target);
2102
+ if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2103
+ const optional2 = def.required ? "" : "?";
2104
+ const relationField = key.replace(/Id$/, "");
2105
+ const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
2106
+ const relationArgs = [
2107
+ ...named?.relationName ? [q(named.relationName)] : [],
2108
+ `fields: [${key}]`,
2109
+ "references: [id]",
2110
+ `onDelete: ${onDeleteAction(def.onDelete)}`
2111
+ ];
2112
+ lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
2113
+ lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
2114
+ indexes.push(` @@index([${key}])`);
2115
+ continue;
2116
+ }
2117
+ const optional = def.required || def.kind === "multiselect" ? "" : "?";
2118
+ lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
2119
+ }
2120
+ for (const back of backReferences(resource, all)) {
2121
+ const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
2122
+ lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
2123
+ }
2124
+ lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
2125
+ lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
2126
+ indexes.push(` @@index([createdAt])`);
2127
+ const sortField = resource.defaultSort.field;
2128
+ if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
2129
+ for (const [key, def] of storedFields(resource)) {
2130
+ if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
2131
+ }
2132
+ lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
2133
+ return lines.join("\n");
2134
+ }
2135
+ function renderPrismaSchema(all) {
2136
+ const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2137
+ const blocks = [];
2138
+ for (const entry of ordered) {
2139
+ blocks.push(...renderPrismaEnums(entry.resource));
2140
+ blocks.push(renderPrismaModel(entry, all));
2141
+ }
2142
+ return `${blocks.join("\n\n")}
2143
+ `;
2144
+ }
2145
+
2146
+ // src/generator/render.ts
2147
+ import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
2148
+ var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
2149
+ var tableExport = (resource) => camelCase4(resource.table);
2150
+ var q2 = (value) => JSON.stringify(value);
1944
2151
  var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
1945
2152
  var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
1946
2153
  function columnExpression(key, def, resources, selfName) {
1947
- const col = q(columnName(key));
2154
+ const col = q2(columnName2(key));
1948
2155
  let expr;
1949
2156
  switch (def.kind) {
1950
2157
  case "int":
@@ -1957,10 +2164,10 @@ function columnExpression(key, def, resources, selfName) {
1957
2164
  expr = `integer(${col}, { mode: "boolean" })`;
1958
2165
  break;
1959
2166
  case "enum":
1960
- expr = `text(${col}, { enum: ${q(def.options)} })`;
2167
+ expr = `text(${col}, { enum: ${q2(def.options)} })`;
1961
2168
  break;
1962
2169
  case "multiselect":
1963
- expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q(option)).join(" | ")})[]>()`;
2170
+ expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
1964
2171
  break;
1965
2172
  case "belongsTo": {
1966
2173
  const target = resources.get(def.target);
@@ -1968,7 +2175,7 @@ function columnExpression(key, def, resources, selfName) {
1968
2175
  throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
1969
2176
  }
1970
2177
  const onDelete = def.onDelete ?? "restrict";
1971
- expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q(onDelete)} })`;
2178
+ expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
1972
2179
  break;
1973
2180
  }
1974
2181
  default:
@@ -1976,13 +2183,13 @@ function columnExpression(key, def, resources, selfName) {
1976
2183
  }
1977
2184
  if (def.required) expr += ".notNull()";
1978
2185
  if ("unique" in def && def.unique) expr += ".unique()";
1979
- if ("default" in def && def.default !== void 0) expr += `.default(${q(def.default)})`;
2186
+ if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
1980
2187
  return expr;
1981
2188
  }
1982
2189
  function renderTableModule(entry, all) {
1983
2190
  const { resource } = entry;
1984
2191
  const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
1985
- const fields = storedFields(resource);
2192
+ const fields = storedFields2(resource);
1986
2193
  const name = tableExport(resource);
1987
2194
  const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
1988
2195
  const extras = [];
@@ -1990,12 +2197,12 @@ function renderTableModule(entry, all) {
1990
2197
  const addIndex = (key, column) => {
1991
2198
  if (indexed.has(key)) return;
1992
2199
  indexed.add(key);
1993
- extras.push(` index(${q(`${resource.table}_${columnName(column)}_idx`)}).on(table.${key}),`);
2200
+ extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
1994
2201
  };
1995
2202
  for (const [key, def] of fields) {
1996
2203
  if (def.kind === "enum") {
1997
2204
  const options2 = def.options.map(sqlString).join(", ");
1998
- extras.push(` check(${q(`${resource.table}_${columnName(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
2205
+ extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
1999
2206
  if (def.filterable !== false) addIndex(key, key);
2000
2207
  }
2001
2208
  if (def.kind === "belongsTo") addIndex(key, key);
@@ -2017,7 +2224,7 @@ function renderTableModule(entry, all) {
2017
2224
  ...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
2018
2225
  "",
2019
2226
  `export const ${name} = sqliteTable(`,
2020
- ` ${q(resource.table)},`,
2227
+ ` ${q2(resource.table)},`,
2021
2228
  " {",
2022
2229
  ` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
2023
2230
  ...columns.map(
@@ -2057,12 +2264,12 @@ function renderRelations(all) {
2057
2264
  const target = byName.get(relation.target);
2058
2265
  used.add(target);
2059
2266
  const other = tableExport(target);
2060
- return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q(relation.relationName)} }),`;
2267
+ return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
2061
2268
  }),
2062
2269
  ...hasMany.map((relation) => {
2063
2270
  const target = byName.get(relation.target);
2064
2271
  used.add(target);
2065
- return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q(relation.relationName)} }),`;
2272
+ return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
2066
2273
  })
2067
2274
  ];
2068
2275
  const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
@@ -2075,32 +2282,40 @@ ${lines.join("\n")}
2075
2282
  const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
2076
2283
  return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
2077
2284
  }
2078
- var handlerImports = (entry) => [
2079
- `import { createResourceHandlers } from "@flaredev/core/server";`,
2080
- `import { getDb } from "@/db";`,
2081
- `import { ${tableExport(entry.resource)} } from "@/db/schema";`,
2082
- `import { authorize, currentUser } from "@/lib/api";`,
2083
- `import { revalidateResource } from "@/lib/cache";`,
2084
- `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`,
2085
- "",
2086
- `const handlers = createResourceHandlers({`,
2087
- ` resource: ${resourceLocal(entry.stem)},`,
2088
- ` table: ${tableExport(entry.resource)},`,
2089
- ` getDb,`,
2090
- ` authorize,`,
2091
- ` currentUser,`,
2092
- ` onChange: revalidateResource,`,
2093
- `});`,
2094
- ""
2095
- ];
2096
- function renderCollectionRoute(entry) {
2097
- return [...handlerImports(entry), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
2285
+ var handlerImports = (entry, stack = "cloudflare") => {
2286
+ const local = resourceLocal(entry.stem);
2287
+ const source = stack === "next" ? {
2288
+ imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
2289
+ wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2290
+ } : {
2291
+ imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2292
+ wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
2293
+ };
2294
+ return [
2295
+ `import { createResourceHandlers } from "@flaredev/core/server";`,
2296
+ ...source.imports,
2297
+ `import { authorize, currentUser } from "@/lib/api";`,
2298
+ `import { revalidateResource } from "@/lib/cache";`,
2299
+ `import ${local} from "@/resources/${entry.stem}.resource";`,
2300
+ "",
2301
+ `const handlers = createResourceHandlers({`,
2302
+ ` resource: ${local},`,
2303
+ ...source.wiring,
2304
+ ` authorize,`,
2305
+ ` currentUser,`,
2306
+ ` onChange: revalidateResource,`,
2307
+ `});`,
2308
+ ""
2309
+ ];
2310
+ };
2311
+ function renderCollectionRoute(entry, stack = "cloudflare") {
2312
+ return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
2098
2313
  "\n"
2099
2314
  );
2100
2315
  }
2101
- function renderItemRoute(entry) {
2316
+ function renderItemRoute(entry, stack = "cloudflare") {
2102
2317
  return [
2103
- ...handlerImports(entry),
2318
+ ...handlerImports(entry, stack),
2104
2319
  "export const GET = handlers.item.GET;",
2105
2320
  "export const PATCH = handlers.item.PATCH;",
2106
2321
  "export const PUT = handlers.item.PUT;",
@@ -2179,7 +2394,7 @@ function renderClient(entry) {
2179
2394
  `import { createResourceClient } from "@flaredev/core/client";`,
2180
2395
  `import type ${local} from "./${stem}.resource";`,
2181
2396
  "",
2182
- `export const ${camelCase3(stem)}Client = createResourceClient<typeof ${local}>(${q(`/api/${resource.slug}`)});`,
2397
+ `export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
2183
2398
  "",
2184
2399
  `export type ${resource.name} = typeof ${local}.$types.record;`,
2185
2400
  `export type ${resource.name}Create = typeof ${local}.$types.create;`,
@@ -2193,7 +2408,7 @@ function renderValidators(entry) {
2193
2408
  `import { createValidators } from "@flaredev/core";`,
2194
2409
  `import ${local} from "./${entry.stem}.resource";`,
2195
2410
  "",
2196
- `export const ${camelCase3(entry.stem)}Validators = createValidators(${local});`,
2411
+ `export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
2197
2412
  ""
2198
2413
  ].join("\n");
2199
2414
  }
@@ -2223,12 +2438,31 @@ function renderServerRegistry(all) {
2223
2438
  ""
2224
2439
  ].join("\n");
2225
2440
  }
2226
- function resourceFiles(entry, all) {
2441
+ function renderPrismaServerRegistry(all) {
2442
+ if (all.length === 0) return "export const resourceTables = {} as const;\n";
2443
+ const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2444
+ return [
2445
+ `import { prisma } from "@/lib/db";`,
2446
+ `import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
2447
+ "",
2448
+ "export const resourceTables = {",
2449
+ ...sorted.map(
2450
+ ({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
2451
+ ),
2452
+ "} as const;",
2453
+ "",
2454
+ "export type ResourceName = keyof typeof resourceTables;",
2455
+ ""
2456
+ ].join("\n");
2457
+ }
2458
+ function resourceFiles(entry, all, stack = "cloudflare") {
2227
2459
  const { resource, stem } = entry;
2228
2460
  return [
2229
- { path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) },
2230
- { path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry) },
2231
- { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry) },
2461
+ // The schema is the one file that belongs to a stack. On Next.js every model lives
2462
+ // in one prisma/schema.prisma, written by planFiles rather than per resource.
2463
+ ...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
2464
+ { path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
2465
+ { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
2232
2466
  { path: `resources/${stem}.client.ts`, content: renderClient(entry) },
2233
2467
  { path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
2234
2468
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
@@ -2243,21 +2477,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
2243
2477
  // rewritten on regeneration; add your own code outside it.
2244
2478
  `;
2245
2479
  var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
2246
- function planFiles(all, policies) {
2480
+ function planFiles(all, policies, stack = "cloudflare") {
2247
2481
  const files = [];
2248
2482
  for (const entry of all) {
2249
- for (const file of resourceFiles(entry, all)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
2250
- }
2251
- files.push(
2252
- { path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
2253
- { path: "resources/index.ts", content: renderRegistry(all), header: "// Registry of every resource descriptor (maintained by flare gen).\n" },
2254
- {
2255
- path: "resources/server.ts",
2256
- content: renderServerRegistry(all),
2257
- header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
2258
- },
2259
- { path: "db/schema.ts", content: renderSchemaIndex(all) }
2260
- );
2483
+ for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
2484
+ }
2485
+ files.push({
2486
+ path: "resources/index.ts",
2487
+ content: renderRegistry(all),
2488
+ header: "// Registry of every resource descriptor (maintained by flare gen).\n"
2489
+ });
2490
+ if (stack === "next") {
2491
+ files.push(
2492
+ // Prisma keeps every model in one schema and writes its own migrations, so there
2493
+ // is no per-table module and no relations file to maintain.
2494
+ {
2495
+ path: "prisma/schema/resources.prisma",
2496
+ content: renderPrismaSchema(all),
2497
+ header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
2498
+ },
2499
+ {
2500
+ path: "resources/server.ts",
2501
+ content: renderPrismaServerRegistry(all),
2502
+ header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
2503
+ }
2504
+ );
2505
+ } else {
2506
+ files.push(
2507
+ { path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
2508
+ {
2509
+ path: "resources/server.ts",
2510
+ content: renderServerRegistry(all),
2511
+ header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
2512
+ },
2513
+ { path: "db/schema.ts", content: renderSchemaIndex(all) }
2514
+ );
2515
+ }
2261
2516
  if (policies) {
2262
2517
  files.push({
2263
2518
  path: "policies/index.ts",
@@ -2269,7 +2524,7 @@ function planFiles(all, policies) {
2269
2524
  }
2270
2525
  function applyPlan(appRoot, plan, options2 = {}) {
2271
2526
  return plan.map(({ path, content, header }) => {
2272
- const absolute = join10(appRoot, path);
2527
+ const absolute = join11(appRoot, path);
2273
2528
  const status = inspectGenerated(absolute, content);
2274
2529
  if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
2275
2530
  if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
@@ -2288,30 +2543,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
2288
2543
  function findOrphans(appRoot, all) {
2289
2544
  const planned = new Set(planFiles(all).map((file) => file.path));
2290
2545
  const candidates = [];
2291
- const list2 = (dir, match) => existsSync11(join10(appRoot, dir)) ? readdirSync6(join10(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2546
+ const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2292
2547
  candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
2293
2548
  candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
2294
2549
  for (const dir of list2("app/api", () => true)) {
2295
- for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
2550
+ for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2296
2551
  }
2297
2552
  for (const dir of list2("app/dashboard", () => true)) {
2298
2553
  for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2299
- if (existsSync11(join10(appRoot, file))) candidates.push(file);
2554
+ if (existsSync12(join11(appRoot, file))) candidates.push(file);
2300
2555
  }
2301
2556
  }
2302
2557
  const orphans = [];
2303
2558
  for (const path of candidates) {
2304
- const match = HEADER_PATTERN.exec(readFileSync7(join10(appRoot, path), "utf8"));
2559
+ const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
2305
2560
  if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
2306
2561
  }
2307
2562
  return orphans;
2308
2563
  }
2309
2564
  function ensureSupportFiles(appRoot, log2) {
2310
2565
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2311
- const target = join10(appRoot, path);
2312
- if (existsSync11(target)) continue;
2566
+ const target = join11(appRoot, path);
2567
+ if (existsSync12(target)) continue;
2313
2568
  mkdirSync4(dirname5(target), { recursive: true });
2314
- copyFileSync2(join10(templatesDir, "app", path), target);
2569
+ copyFileSync2(join11(templatesDir, "app", path), target);
2315
2570
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2316
2571
  }
2317
2572
  }
@@ -2339,15 +2594,15 @@ function billingFiles(appName) {
2339
2594
  }
2340
2595
  var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
2341
2596
  function appendVars(appRoot, target) {
2342
- const path = join11(appRoot, target);
2343
- const text = existsSync12(path) ? readFileSync8(path, "utf8") : "";
2597
+ const path = join12(appRoot, target);
2598
+ const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
2344
2599
  if (/STRIPE_SECRET_KEY/.test(text)) return false;
2345
2600
  appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
2346
2601
  return true;
2347
2602
  }
2348
2603
  function addStripeDependency(appRoot) {
2349
- const path = join11(appRoot, "package.json");
2350
- const pkg = JSON.parse(readFileSync8(path, "utf8"));
2604
+ const path = join12(appRoot, "package.json");
2605
+ const pkg = JSON.parse(readFileSync9(path, "utf8"));
2351
2606
  pkg.dependencies ??= {};
2352
2607
  if (pkg.dependencies.stripe) return false;
2353
2608
  pkg.dependencies.stripe = STRIPE_DEP;
@@ -2356,14 +2611,14 @@ function addStripeDependency(appRoot) {
2356
2611
  }
2357
2612
  function writeBillingResource(appRoot, name, force, log2) {
2358
2613
  const relative2 = descriptorPath(name);
2359
- const path = join11(appRoot, relative2);
2360
- if (!existsSync12(path)) {
2614
+ const path = join12(appRoot, relative2);
2615
+ if (!existsSync13(path)) {
2361
2616
  mkdirSync5(dirname6(path), { recursive: true });
2362
- writeFileSync6(path, renderBillingDescriptor(name));
2617
+ writeFileSync7(path, renderBillingDescriptor(name));
2363
2618
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2364
2619
  return;
2365
2620
  }
2366
- const parts = splitMarkers(readFileSync8(path, "utf8"));
2621
+ const parts = splitMarkers(readFileSync9(path, "utf8"));
2367
2622
  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]}`);
2368
2623
  const merged = mergeBillingFields(name, parts.block);
2369
2624
  if (!merged) {
@@ -2387,26 +2642,26 @@ async function genBilling(options2 = {}) {
2387
2642
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2388
2643
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2389
2644
  const relative2 = policyPath(name);
2390
- if (existsSync12(join11(appRoot, relative2))) {
2645
+ if (existsSync13(join12(appRoot, relative2))) {
2391
2646
  log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2392
2647
  continue;
2393
2648
  }
2394
- mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2395
- writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2649
+ mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2650
+ writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2396
2651
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2397
2652
  }
2398
2653
  const files = [];
2399
- const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2654
+ const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2400
2655
  for (const { path, content } of billingFiles(appName)) {
2401
- const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2656
+ const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
2402
2657
  files.push({ path, status: "missing", outcome });
2403
2658
  const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2404
2659
  log2(`${label.padEnd(18)} ${path}`);
2405
2660
  }
2406
2661
  for (const path of RETIRED_FILES) {
2407
- const absolute = join11(appRoot, path);
2408
- if (!existsSync12(absolute)) continue;
2409
- const parts = splitMarkers(readFileSync8(absolute, "utf8"));
2662
+ const absolute = join12(appRoot, path);
2663
+ if (!existsSync13(absolute)) continue;
2664
+ const parts = splitMarkers(readFileSync9(absolute, "utf8"));
2410
2665
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2411
2666
  if (untouched) {
2412
2667
  unlinkSync(absolute);
@@ -2427,7 +2682,7 @@ async function genBilling(options2 = {}) {
2427
2682
  }
2428
2683
  const all = await loadResources(appRoot);
2429
2684
  ensureSupportFiles(appRoot, log2);
2430
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2685
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
2431
2686
  let migrations = [];
2432
2687
  if (!options2.skipMigration) {
2433
2688
  const result = await generateSchemaMigration(appRoot, "billing");
@@ -2450,8 +2705,8 @@ async function genBilling(options2 = {}) {
2450
2705
  }
2451
2706
 
2452
2707
  // src/commands/gen-security.ts
2453
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2454
- import { dirname as dirname7, join as join12 } from "path";
2708
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
2709
+ import { dirname as dirname7, join as join13 } from "path";
2455
2710
  import pc7 from "picocolors";
2456
2711
 
2457
2712
  // src/generator/security.ts
@@ -2979,28 +3234,28 @@ function securityFiles(appName) {
2979
3234
  }
2980
3235
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2981
3236
  function writeOnce(appRoot, relative2, content, log2) {
2982
- const path = join12(appRoot, relative2);
2983
- if (existsSync13(path)) {
3237
+ const path = join13(appRoot, relative2);
3238
+ if (existsSync14(path)) {
2984
3239
  log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2985
3240
  return;
2986
3241
  }
2987
3242
  mkdirSync6(dirname7(path), { recursive: true });
2988
- writeFileSync7(path, content);
3243
+ writeFileSync8(path, content);
2989
3244
  log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
2990
3245
  }
2991
3246
  async function genSecurity(options2 = {}) {
2992
3247
  const log2 = options2.log ?? ((message) => console.log(message));
2993
3248
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2994
- const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
3249
+ const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
2995
3250
  writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
2996
3251
  writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
2997
3252
  writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
2998
3253
  const files = [];
2999
3254
  const write = (path, content, header) => {
3000
- const absolute = join12(appRoot, path);
3001
- if (path === "lib/security.ts" && existsSync13(absolute)) {
3002
- const source = readFileSync9(absolute, "utf8");
3003
- if (PASS_THROUGH_HEADER.test(source)) writeFileSync7(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
3255
+ const absolute = join13(appRoot, path);
3256
+ if (path === "lib/security.ts" && existsSync14(absolute)) {
3257
+ const source = readFileSync10(absolute, "utf8");
3258
+ if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
3004
3259
  }
3005
3260
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
3006
3261
  files.push({ path, status: "missing", outcome });
@@ -3008,16 +3263,16 @@ async function genSecurity(options2 = {}) {
3008
3263
  log2(`${label.padEnd(18)} ${path}`);
3009
3264
  };
3010
3265
  for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
3011
- const navPath = join12(appRoot, "lib/dashboard-nav.ts");
3012
- if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
3266
+ const navPath = join13(appRoot, "lib/dashboard-nav.ts");
3267
+ if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
3013
3268
  else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
3014
- const wranglerPath = join12(appRoot, "wrangler.jsonc");
3269
+ const wranglerPath = join13(appRoot, "wrangler.jsonc");
3015
3270
  let bindings = [];
3016
- if (existsSync13(wranglerPath)) {
3017
- const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
3271
+ if (existsSync14(wranglerPath)) {
3272
+ const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
3018
3273
  bindings = added;
3019
3274
  if (added.length) {
3020
- writeFileSync7(wranglerPath, text);
3275
+ writeFileSync8(wranglerPath, text);
3021
3276
  for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
3022
3277
  }
3023
3278
  } else {
@@ -3025,7 +3280,7 @@ async function genSecurity(options2 = {}) {
3025
3280
  }
3026
3281
  const all = await loadResources(appRoot);
3027
3282
  ensureSupportFiles(appRoot, log2);
3028
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
3283
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
3029
3284
  let migrations = [];
3030
3285
  if (!options2.skipMigration) {
3031
3286
  const result = await generateSchemaMigration(appRoot, "security");
@@ -3048,15 +3303,15 @@ async function genSecurity(options2 = {}) {
3048
3303
  }
3049
3304
 
3050
3305
  // src/commands/theme.ts
3051
- import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3052
- import { join as join13 } from "path";
3306
+ import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3307
+ import { join as join14 } from "path";
3053
3308
  import pc8 from "picocolors";
3054
3309
  var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
3055
3310
  function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3056
3311
  const appRoot = findAppRoot(cwd);
3057
- const path = join13(appRoot, "lib/site.ts");
3058
- if (!existsSync14(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
3059
- const source = readFileSync10(path, "utf8");
3312
+ const path = join14(appRoot, "lib/site.ts");
3313
+ if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
3314
+ const source = readFileSync11(path, "utf8");
3060
3315
  const current = THEME_LINE.exec(source)?.[1];
3061
3316
  if (!name) {
3062
3317
  for (const [theme2, description] of Object.entries(THEMES)) {
@@ -3067,7 +3322,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3067
3322
  }
3068
3323
  const theme = parseTheme(name);
3069
3324
  if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
3070
- writeFileSync8(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3325
+ writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3071
3326
  log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3072
3327
  }
3073
3328
 
@@ -3100,6 +3355,15 @@ function stopIfCancelled(value) {
3100
3355
  async function askCreateQuestions(given, appName) {
3101
3356
  prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
3102
3357
  const answers = { ...given };
3358
+ if (given.stack === void 0) {
3359
+ answers.stack = stopIfCancelled(
3360
+ await prompts2.select({
3361
+ message: "Where will it run?",
3362
+ initialValue: "cloudflare",
3363
+ options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
3364
+ })
3365
+ );
3366
+ }
3103
3367
  if (given.theme === void 0) {
3104
3368
  answers.theme = stopIfCancelled(
3105
3369
  await prompts2.select({
@@ -3139,9 +3403,9 @@ async function askCreateQuestions(given, appName) {
3139
3403
  }
3140
3404
 
3141
3405
  // src/commands/gen.ts
3142
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
3143
- import { dirname as dirname8, join as join14 } from "path";
3144
- import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3406
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
3407
+ import { dirname as dirname8, join as join15 } from "path";
3408
+ import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3145
3409
  import pc10 from "picocolors";
3146
3410
  function validateFields(name, fields, others, log2) {
3147
3411
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
@@ -3153,7 +3417,7 @@ function validateFields(name, fields, others, log2) {
3153
3417
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
3154
3418
  const graph = relationGraph2([...others, candidate]);
3155
3419
  for (const pending of graph.pending.filter((p) => p.resource === name)) {
3156
- log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
3420
+ log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
3157
3421
  }
3158
3422
  }
3159
3423
  async function genResource(rawName, options2) {
@@ -3161,8 +3425,8 @@ async function genResource(rawName, options2) {
3161
3425
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3162
3426
  const name = resourceName(rawName);
3163
3427
  const relativePath = descriptorPath(name);
3164
- const path = join14(appRoot, relativePath);
3165
- const exists = existsSync15(path);
3428
+ const path = join15(appRoot, relativePath);
3429
+ const exists = existsSync16(path);
3166
3430
  if (!exists && !options2.fields) {
3167
3431
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
3168
3432
  }
@@ -3173,7 +3437,7 @@ async function genResource(rawName, options2) {
3173
3437
  validateFields(name, fields, others, log2);
3174
3438
  if (!exists) {
3175
3439
  mkdirSync7(dirname8(path), { recursive: true });
3176
- writeFileSync9(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3440
+ writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3177
3441
  log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
3178
3442
  } else {
3179
3443
  try {
@@ -3195,7 +3459,7 @@ async function genResource(rawName, options2) {
3195
3459
  const all = await loadResources(appRoot);
3196
3460
  table = all.find(({ resource }) => resource.name === name).resource.table;
3197
3461
  ensureSupportFiles(appRoot, log2);
3198
- written = applyPlan(appRoot, planFiles(all), { force: options2.force });
3462
+ written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
3199
3463
  logResults(written, log2);
3200
3464
  } catch (error) {
3201
3465
  const hint = exists ? "" : `
@@ -3209,22 +3473,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
3209
3473
  }
3210
3474
  let migrations = [];
3211
3475
  if (!options2.skipMigration) {
3212
- const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3213
- migrations = result.files;
3214
- for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3215
- for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3216
- if (migrations.length) log2(`
3476
+ if (readStack(appRoot) === "next") {
3477
+ log2(`
3478
+ Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
3479
+ } else {
3480
+ const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3481
+ migrations = result.files;
3482
+ for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3483
+ for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3484
+ if (migrations.length) log2(`
3217
3485
  Next: ${pc10.bold("flare migrate")} to apply it locally.`);
3218
- else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3486
+ else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3487
+ }
3219
3488
  }
3220
3489
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
3221
3490
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
3222
3491
  }
3223
3492
 
3224
3493
  // src/commands/gen-endpoint.ts
3225
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
3226
- import { join as join15 } from "path";
3227
- import { kebabCase as kebabCase3, pascalCase as pascalCase2 } from "@flaredev/core";
3494
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
3495
+ import { join as join16 } from "path";
3496
+ import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
3228
3497
  import pc11 from "picocolors";
3229
3498
  var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
3230
3499
  var ACTIONS = ["read", "create", "update", "delete"];
@@ -3236,7 +3505,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3236
3505
  const name = kebabCase3(rawName);
3237
3506
  if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
3238
3507
  const resources = await loadResources(appRoot);
3239
- const wanted = pascalCase2(resourceName2);
3508
+ const wanted = pascalCase3(resourceName2);
3240
3509
  const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
3241
3510
  if (!entry) {
3242
3511
  throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
@@ -3245,11 +3514,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3245
3514
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3246
3515
  const { resource, stem } = entry;
3247
3516
  const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3248
- const path = join15(appRoot, relative2);
3249
- if (existsSync16(path)) throw new Error(`${relative2} already exists.`);
3250
- mkdirSync8(join15(path, ".."), { recursive: true });
3517
+ const path = join16(appRoot, relative2);
3518
+ if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
3519
+ mkdirSync8(join16(path, ".."), { recursive: true });
3251
3520
  const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3252
- writeFileSync10(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3521
+ writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3253
3522
  log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3254
3523
  log2(`
3255
3524
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
@@ -3285,8 +3554,8 @@ ${body}
3285
3554
  }
3286
3555
 
3287
3556
  // src/commands/gen-migration.ts
3288
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
3289
- import { basename as basename2, join as join16 } from "path";
3557
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
3558
+ import { basename as basename2, join as join17 } from "path";
3290
3559
  import { snakeCase } from "@flaredev/core";
3291
3560
  import pc12 from "picocolors";
3292
3561
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
@@ -3307,12 +3576,12 @@ async function genMigration(rawName, options2 = {}) {
3307
3576
  for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
3308
3577
  for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
3309
3578
  if (!options2.fromSchema) {
3310
- const downDir = join16(appRoot, "migrations", "down");
3579
+ const downDir = join17(appRoot, "migrations", "down");
3311
3580
  mkdirSync9(downDir, { recursive: true });
3312
3581
  for (const file of files) {
3313
- const down = join16(downDir, basename2(file));
3314
- if (existsSync17(down)) continue;
3315
- writeFileSync11(down, DOWN_TEMPLATE(basename2(file)));
3582
+ const down = join17(downDir, basename2(file));
3583
+ if (existsSync18(down)) continue;
3584
+ writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
3316
3585
  log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3317
3586
  }
3318
3587
  log2(`
@@ -3325,8 +3594,8 @@ Next: ${pc12.bold("flare migrate")}.`);
3325
3594
  }
3326
3595
 
3327
3596
  // src/commands/gen-policy.ts
3328
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
3329
- import { dirname as dirname9, join as join17 } from "path";
3597
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
3598
+ import { dirname as dirname9, join as join18 } from "path";
3330
3599
  import { definePolicy } from "@flaredev/core";
3331
3600
  import pc13 from "picocolors";
3332
3601
  function parseRoles(input, what = "--roles") {
@@ -3344,8 +3613,8 @@ async function genPolicy(rawName, options2) {
3344
3613
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3345
3614
  }
3346
3615
  const relative2 = policyPath(name);
3347
- const path = join17(appRoot, relative2);
3348
- const exists = existsSync18(path);
3616
+ const path = join18(appRoot, relative2);
3617
+ const exists = existsSync19(path);
3349
3618
  let roles;
3350
3619
  let deleteRoles;
3351
3620
  if (options2.roles || !exists) {
@@ -3357,7 +3626,7 @@ async function genPolicy(rawName, options2) {
3357
3626
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
3358
3627
  if (!exists) {
3359
3628
  mkdirSync10(dirname9(path), { recursive: true });
3360
- writeFileSync12(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3629
+ writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3361
3630
  log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3362
3631
  } else {
3363
3632
  try {
@@ -3372,16 +3641,16 @@ async function genPolicy(rawName, options2) {
3372
3641
  }
3373
3642
  }
3374
3643
  const policies = await loadPolicies(appRoot);
3375
- logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
3644
+ logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3376
3645
  log2(`
3377
3646
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3378
3647
  return { name, path: relative2 };
3379
3648
  }
3380
3649
 
3381
3650
  // src/commands/migrate.ts
3382
- import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
3651
+ import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
3383
3652
  import { tmpdir } from "os";
3384
- import { join as join18 } from "path";
3653
+ import { join as join19 } from "path";
3385
3654
  import pc14 from "picocolors";
3386
3655
 
3387
3656
  // src/generator/down.ts
@@ -3475,7 +3744,17 @@ function context(options2) {
3475
3744
  };
3476
3745
  }
3477
3746
  async function migrate(options2 = {}) {
3478
- const { appRoot, db, target, wrangler } = context(options2);
3747
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3748
+ const log2 = options2.log ?? ((message) => console.log(message));
3749
+ if (readStack(appRoot) === "next") {
3750
+ if (options2.remote) {
3751
+ log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
3752
+ }
3753
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
3754
+ log2(`${pc14.cyan("prisma")} migrate deploy`);
3755
+ return runNode([prisma, "migrate", "deploy"], appRoot);
3756
+ }
3757
+ const { db, target, wrangler } = context(options2);
3479
3758
  const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
3480
3759
  return result.code;
3481
3760
  }
@@ -3499,7 +3778,7 @@ async function rollback(options2 = {}) {
3499
3778
  const steps = options2.steps ?? 1;
3500
3779
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3501
3780
  const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
3502
- const migrationsDir = join18(appRoot, db.migrations_dir ?? "migrations");
3781
+ const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
3503
3782
  const applied = await appliedMigrations(options2, steps);
3504
3783
  if (applied.length === 0) {
3505
3784
  log2("Nothing to roll back.");
@@ -3508,18 +3787,18 @@ async function rollback(options2 = {}) {
3508
3787
  const plan = [];
3509
3788
  for (const { name } of applied) {
3510
3789
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
3511
- const handWritten = join18(migrationsDir, "down", name);
3512
- if (existsSync19(handWritten)) {
3513
- const sql2 = readFileSync11(handWritten, "utf8");
3790
+ const handWritten = join19(migrationsDir, "down", name);
3791
+ if (existsSync20(handWritten)) {
3792
+ const sql2 = readFileSync12(handWritten, "utf8");
3514
3793
  if (splitStatements(sql2).length === 0) {
3515
3794
  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.`);
3516
3795
  }
3517
3796
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3518
3797
  continue;
3519
3798
  }
3520
- const upPath = join18(migrationsDir, name);
3521
- if (!existsSync19(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3522
- const down = deriveDown(readFileSync11(upPath, "utf8"));
3799
+ const upPath = join19(migrationsDir, name);
3800
+ if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3801
+ const down = deriveDown(readFileSync12(upPath, "utf8"));
3523
3802
  if (!down.reversible) {
3524
3803
  throw new Error(
3525
3804
  `Can't roll back ${name}: ${down.reason}
@@ -3539,15 +3818,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3539
3818
  }
3540
3819
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3541
3820
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3542
- const dir = mkdtempSync(join18(tmpdir(), "flare-rollback-"));
3821
+ const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
3543
3822
  try {
3544
- const file = join18(dir, "rollback.sql");
3545
- writeFileSync13(file, script);
3823
+ const file = join19(dir, "rollback.sql");
3824
+ writeFileSync14(file, script);
3546
3825
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3547
3826
  if (result.code !== 0) throw new Error(`Rollback failed:
3548
3827
  ${result.output.trim()}`);
3549
3828
  } finally {
3550
- rmSync2(dir, { recursive: true, force: true });
3829
+ rmSync3(dir, { recursive: true, force: true });
3551
3830
  }
3552
3831
  log2(pc14.green(`
3553
3832
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
@@ -3555,8 +3834,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3555
3834
  }
3556
3835
 
3557
3836
  // src/commands/rm.ts
3558
- import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3559
- import { dirname as dirname10, join as join19 } from "path";
3837
+ import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3838
+ import { dirname as dirname10, join as join20 } from "path";
3560
3839
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3561
3840
  import pc15 from "picocolors";
3562
3841
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
@@ -3575,10 +3854,10 @@ function handWrittenReason(source, header, options2 = {}) {
3575
3854
  return null;
3576
3855
  }
3577
3856
  function removeEmptyDirs(appRoot, start) {
3578
- let dir = dirname10(join19(appRoot, start));
3579
- const roots = [join19(appRoot, "app", "api"), join19(appRoot, "app", "dashboard")];
3857
+ let dir = dirname10(join20(appRoot, start));
3858
+ const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
3580
3859
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3581
- while (inside() && existsSync20(dir) && readdirSync7(dir).length === 0) {
3860
+ while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
3582
3861
  rmdirSync(dir);
3583
3862
  dir = dirname10(dir);
3584
3863
  }
@@ -3606,8 +3885,8 @@ async function rmResource(rawName, options2 = {}) {
3606
3885
  const owned = /* @__PURE__ */ new Set();
3607
3886
  if (entry) {
3608
3887
  owned.add(entry.file);
3609
- for (const file of planFiles(all)) {
3610
- if (existsSync20(join19(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3888
+ for (const file of planFiles(all, void 0, readStack(appRoot))) {
3889
+ if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3611
3890
  }
3612
3891
  }
3613
3892
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3616,7 +3895,7 @@ async function rmResource(rawName, options2 = {}) {
3616
3895
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3617
3896
  const findings = [];
3618
3897
  for (const path of owned) {
3619
- const source = readFileSync12(join19(appRoot, path), "utf8");
3898
+ const source = readFileSync13(join20(appRoot, path), "utf8");
3620
3899
  let wrapper;
3621
3900
  if (entry && path === entry.file) {
3622
3901
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3637,15 +3916,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
3637
3916
  }
3638
3917
  const removed = [...owned].sort();
3639
3918
  for (const path of removed) {
3640
- unlinkSync2(join19(appRoot, path));
3919
+ unlinkSync2(join20(appRoot, path));
3641
3920
  removeEmptyDirs(appRoot, path);
3642
3921
  log2(`${pc15.red("remove".padEnd(9))} ${path}`);
3643
3922
  }
3644
- logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
3923
+ logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
3645
3924
  const table = entry ? tableExport(entry.resource) : void 0;
3646
- if (table && existsSync20(join19(appRoot, "seeds"))) {
3647
- const seeds = readdirSync7(join19(appRoot, "seeds")).filter(
3648
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join19(appRoot, "seeds", file), "utf8"))
3925
+ if (table && existsSync21(join20(appRoot, "seeds"))) {
3926
+ const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
3927
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
3649
3928
  );
3650
3929
  if (seeds.length) log2(pc15.yellow(`
3651
3930
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
@@ -3715,9 +3994,9 @@ ${result.output.trim()}`);
3715
3994
  }
3716
3995
 
3717
3996
  // src/commands/seed.ts
3718
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "fs";
3719
- import { join as join21 } from "path";
3720
- import { createFake, kebabCase as kebabCase4, storedFields as storedFields2 } from "@flaredev/core";
3997
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
3998
+ import { join as join22 } from "path";
3999
+ import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
3721
4000
  import { createJiti as createJiti4 } from "jiti";
3722
4001
  import pc17 from "picocolors";
3723
4002
 
@@ -3814,16 +4093,16 @@ function createInsertMany(sink, drizzle, onProgress) {
3814
4093
 
3815
4094
  // src/seed/local-d1.ts
3816
4095
  import { createRequire } from "module";
3817
- import { join as join20 } from "path";
4096
+ import { join as join21 } from "path";
3818
4097
  import { pathToFileURL } from "url";
3819
4098
  var LOCAL_STATE = ".wrangler/state/v3";
3820
4099
  async function openLocalD1(appRoot, binding) {
3821
4100
  process.env.WRANGLER_LOG ??= "error";
3822
- const appRequire = createRequire(join20(appRoot, "package.json"));
4101
+ const appRequire = createRequire(join21(appRoot, "package.json"));
3823
4102
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3824
4103
  const { env, dispose } = await wrangler.getPlatformProxy({
3825
- configPath: join20(appRoot, "wrangler.jsonc"),
3826
- persist: { path: join20(appRoot, LOCAL_STATE) }
4104
+ configPath: join21(appRoot, "wrangler.jsonc"),
4105
+ persist: { path: join21(appRoot, LOCAL_STATE) }
3827
4106
  });
3828
4107
  const bindings = env;
3829
4108
  const name = binding ?? "DB";
@@ -3838,8 +4117,8 @@ async function openLocalD1(appRoot, binding) {
3838
4117
  // src/commands/seed.ts
3839
4118
  var SEEDS_DIR = "seeds";
3840
4119
  function seedFiles(appRoot, names) {
3841
- const dir = join21(appRoot, SEEDS_DIR);
3842
- const all = existsSync21(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4120
+ const dir = join22(appRoot, SEEDS_DIR);
4121
+ const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3843
4122
  if (!names?.length) return all;
3844
4123
  return names.map((name) => {
3845
4124
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3857,16 +4136,20 @@ async function runSeeds(options2 = {}) {
3857
4136
  log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
3858
4137
  return;
3859
4138
  }
3860
- const jiti = createJiti4(join21(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4139
+ if (readStack(appRoot) === "next") {
4140
+ await runPrismaSeeds(appRoot, files, log2);
4141
+ return;
4142
+ }
4143
+ const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3861
4144
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3862
4145
  const drizzleHelpers = await jiti.import("drizzle-orm");
3863
- const schema = await jiti.import(join21(appRoot, "db/schema.ts"));
4146
+ const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
3864
4147
  const local = await openLocalD1(appRoot);
3865
4148
  try {
3866
4149
  const db = drizzle(local.db, { schema });
3867
4150
  const insertMany = createInsertMany(local.db, drizzleHelpers);
3868
4151
  for (const file of files) {
3869
- const mod = await jiti.import(join21(appRoot, SEEDS_DIR, file));
4152
+ const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
3870
4153
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3871
4154
  log2(`${pc17.cyan("seed")} ${file}`);
3872
4155
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
@@ -3916,9 +4199,17 @@ function sampleValue(key, def) {
3916
4199
  return void 0;
3917
4200
  }
3918
4201
  }
3919
- function renderSeed(resource) {
4202
+ function renderSeed(resource, stack = "cloudflare") {
3920
4203
  if (!resource) {
3921
- return `import { defineSeed } from "@flaredev/core";
4204
+ return stack === "next" ? `import { defineSeed } from "@flaredev/core";
4205
+
4206
+ export default defineSeed(async ({ db, insertMany, fake, log }) => {
4207
+ // \`db\` is this app's Prisma client, e.g.:
4208
+ // await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
4209
+ // Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
4210
+ log("nothing to seed yet");
4211
+ });
4212
+ ` : `import { defineSeed } from "@flaredev/core";
3922
4213
 
3923
4214
  export default defineSeed(async ({ db, insertMany, fake, log }) => {
3924
4215
  // Insert rows with Drizzle, e.g.:
@@ -3931,11 +4222,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
3931
4222
  const table = tableExport(resource);
3932
4223
  const lines = [];
3933
4224
  const todos = [];
3934
- for (const [key, def] of storedFields2(resource)) {
4225
+ for (const [key, def] of storedFields3(resource)) {
3935
4226
  const value = sampleValue(key, def);
3936
4227
  if (value !== void 0) lines.push(` ${key}: ${value},`);
3937
4228
  else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
3938
4229
  }
4230
+ if (stack === "next") {
4231
+ const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
4232
+ return `import { defineSeed } from "@flaredev/core";
4233
+
4234
+ /** How many to create. createMany sends them in one statement. */
4235
+ const COUNT = 50;
4236
+
4237
+ export default defineSeed(async ({ insertMany, fake, log }) => {
4238
+ await insertMany(
4239
+ ${JSON.stringify(camelCase6(resource.name))},
4240
+ Array.from({ length: COUNT }, () => ({
4241
+ ${body}
4242
+ updatedAt: new Date(),
4243
+ })),
4244
+ );
4245
+ log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
4246
+ });
4247
+ `;
4248
+ }
3939
4249
  return `import { defineSeed } from "@flaredev/core";
3940
4250
  import { ${table} } from "@/db/schema";
3941
4251
 
@@ -3956,8 +4266,8 @@ async function makeSeed(rawName, options2 = {}) {
3956
4266
  const name = kebabCase4(rawName);
3957
4267
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3958
4268
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3959
- const path = join21(appRoot, relative2);
3960
- if (existsSync21(path)) throw new Error(`${relative2} already exists.`);
4269
+ const path = join22(appRoot, relative2);
4270
+ if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
3961
4271
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3962
4272
  let resource;
3963
4273
  if (options2.resource) {
@@ -3966,18 +4276,58 @@ async function makeSeed(rawName, options2 = {}) {
3966
4276
  } else {
3967
4277
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
3968
4278
  }
3969
- mkdirSync11(join21(appRoot, SEEDS_DIR), { recursive: true });
3970
- writeFileSync14(path, renderSeed(resource));
4279
+ mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4280
+ writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
3971
4281
  log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
3972
4282
  log2(`
3973
4283
  Run it with ${pc17.bold(`flare seed ${name}`)}.`);
3974
4284
  return relative2;
3975
4285
  }
4286
+ async function runPrismaSeeds(appRoot, files, log2) {
4287
+ const runner = join22(appRoot, ".flare-seed-runner.mts");
4288
+ const script = [
4289
+ `import "dotenv/config";`,
4290
+ `import { createFake } from "@flaredev/core";`,
4291
+ `import { prisma } from "./lib/db";`,
4292
+ ...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
4293
+ "",
4294
+ `const files = ${JSON.stringify(files)};`,
4295
+ `const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
4296
+ `const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
4297
+ "",
4298
+ `for (const [index, seed] of seeds.entries()) {`,
4299
+ ` console.log("seed " + files[index]);`,
4300
+ ` await seed({`,
4301
+ ` db: prisma,`,
4302
+ ` env: process.env,`,
4303
+ ` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
4304
+ ` const delegate = client[model];`,
4305
+ ` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
4306
+ ` await delegate.createMany({ data: rows });`,
4307
+ ` },`,
4308
+ ` fake: createFake(),`,
4309
+ ` log: (message: string) => console.log(" " + message),`,
4310
+ ` });`,
4311
+ `}`,
4312
+ `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4313
+ ""
4314
+ ].join("\n");
4315
+ writeFileSync15(runner, script);
4316
+ try {
4317
+ const tsx = resolveBin(appRoot, "tsx", "tsx");
4318
+ const code = await runNode([tsx, runner], appRoot);
4319
+ if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4320
+ log2(pc17.green(`
4321
+ Ran ${files.length} seed(s).`));
4322
+ } finally {
4323
+ rmSync4(runner, { force: true });
4324
+ }
4325
+ }
3976
4326
 
3977
4327
  // src/commands/db-push.ts
3978
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4328
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
3979
4329
  import { tmpdir as tmpdir2 } from "os";
3980
- import { join as join22 } from "path";
4330
+ import { join as join23 } from "path";
3981
4331
  import pc18 from "picocolors";
3982
4332
 
3983
4333
  // src/terminal.ts
@@ -4042,7 +4392,7 @@ async function dbPush(tables, options2 = {}) {
4042
4392
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4043
4393
  const started = Date.now();
4044
4394
  const local = await openLocalD1(appRoot, options2.database);
4045
- const dir = mkdtempSync2(join22(tmpdir2(), "flare-push-"));
4395
+ const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4046
4396
  try {
4047
4397
  const statements = [];
4048
4398
  const counts = [];
@@ -4068,8 +4418,8 @@ async function dbPush(tables, options2 = {}) {
4068
4418
  log2("Nothing to push: those tables are empty locally.");
4069
4419
  return { tables: counts, ms: Date.now() - started };
4070
4420
  }
4071
- const file = join22(dir, "push.sql");
4072
- writeFileSync15(file, statements.join("\n"));
4421
+ const file = join23(dir, "push.sql");
4422
+ writeFileSync16(file, statements.join("\n"));
4073
4423
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4074
4424
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4075
4425
  if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
@@ -4078,19 +4428,19 @@ ${result.output.trim()}`);
4078
4428
  log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4079
4429
  return { tables: counts, ms };
4080
4430
  } finally {
4081
- rmSync3(dir, { recursive: true, force: true });
4431
+ rmSync5(dir, { recursive: true, force: true });
4082
4432
  await local.dispose();
4083
4433
  }
4084
4434
  }
4085
4435
 
4086
4436
  // src/commands/seed-resource.ts
4087
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4437
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
4088
4438
  import { tmpdir as tmpdir3 } from "os";
4089
- import { join as join23 } from "path";
4439
+ import { join as join24 } from "path";
4090
4440
  import pc19 from "picocolors";
4091
4441
 
4092
4442
  // src/seed/rows.ts
4093
- import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
4443
+ import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
4094
4444
  var has = (key, ...words) => words.some((word) => key.includes(word));
4095
4445
  function stringValue(key, def, fake, resource, index) {
4096
4446
  switch (def.format) {
@@ -4195,12 +4545,12 @@ function digitalish(resource) {
4195
4545
  );
4196
4546
  }
4197
4547
  function seedColumns(resource) {
4198
- return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4548
+ return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
4199
4549
  }
4200
4550
  function* seedRows(resource, count, options2 = {}) {
4201
4551
  const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
4202
4552
  const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
4203
- const fields = storedFields3(resource);
4553
+ const fields = storedFields4(resource);
4204
4554
  const now = options2.now ?? Date.now();
4205
4555
  const start = options2.startIndex ?? 0;
4206
4556
  for (let i = 0; i < count; i++) {
@@ -4209,7 +4559,7 @@ function* seedRows(resource, count, options2 = {}) {
4209
4559
  for (const [key, def] of fields) {
4210
4560
  let value = fieldValue(key, def, fake, resource, parentIds, number);
4211
4561
  if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
4212
- row[columnName2(key)] = value;
4562
+ row[columnName3(key)] = value;
4213
4563
  }
4214
4564
  const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
4215
4565
  row.created_at = created;
@@ -4288,6 +4638,11 @@ async function seedResourceCommand(name, options2 = {}) {
4288
4638
  async function seedResource(name, options2 = {}) {
4289
4639
  const log2 = options2.log ?? ((message) => console.log(message));
4290
4640
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4641
+ if (readStack(appRoot) === "next") {
4642
+ throw new Error(
4643
+ "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`."
4644
+ );
4645
+ }
4291
4646
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4292
4647
  const resource = pickResource(resources, name);
4293
4648
  const total = parseCount(options2.count);
@@ -4320,8 +4675,8 @@ ${result.output.trim()}`);
4320
4675
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4321
4676
  startIndex = counted?.total ?? 0;
4322
4677
  }
4323
- const dir = mkdtempSync3(join23(tmpdir3(), "flare-seed-"));
4324
- const file = join23(dir, `${resource.table}.sql`);
4678
+ const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4679
+ const file = join24(dir, `${resource.table}.sql`);
4325
4680
  try {
4326
4681
  const statements = [];
4327
4682
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4331,13 +4686,13 @@ ${result.output.trim()}`);
4331
4686
  written += statement.rows;
4332
4687
  options2.onProgress?.(written, total);
4333
4688
  }
4334
- writeFileSync16(file, statements.join("\n"));
4689
+ writeFileSync17(file, statements.join("\n"));
4335
4690
  log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4336
4691
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4337
4692
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4338
4693
  ${result.output.trim()}`);
4339
4694
  } finally {
4340
- rmSync4(dir, { recursive: true, force: true });
4695
+ rmSync6(dir, { recursive: true, force: true });
4341
4696
  }
4342
4697
  return { resource, rows: total, ms: Date.now() - started, where };
4343
4698
  }
@@ -4364,17 +4719,17 @@ ${result.output.trim()}`);
4364
4719
 
4365
4720
  // src/commands/billing-sync.ts
4366
4721
  import { createRequire as createRequire2 } from "module";
4367
- import { existsSync as existsSync22, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
4722
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
4368
4723
  import { tmpdir as tmpdir4 } from "os";
4369
- import { join as join24 } from "path";
4724
+ import { join as join25 } from "path";
4370
4725
  import { pathToFileURL as pathToFileURL2 } from "url";
4371
4726
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4372
4727
  import pc20 from "picocolors";
4373
4728
  function stripeKeys(appRoot) {
4374
4729
  const map = {};
4375
- const path = join24(appRoot, ".dev.vars");
4376
- if (existsSync22(path)) {
4377
- for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
4730
+ const path = join25(appRoot, ".dev.vars");
4731
+ if (existsSync23(path)) {
4732
+ for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
4378
4733
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4379
4734
  if (match) map[match[1]] = match[2].trim();
4380
4735
  }
@@ -4457,10 +4812,10 @@ async function syncPlans(options2 = {}) {
4457
4812
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4458
4813
  const db = readD1Databases(appRoot)[0];
4459
4814
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4460
- const appRequire = createRequire2(join24(appRoot, "package.json"));
4815
+ const appRequire = createRequire2(join25(appRoot, "package.json"));
4461
4816
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4462
4817
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4463
- const appName = JSON.parse(readFileSync13(join24(appRoot, "package.json"), "utf8")).name ?? "app";
4818
+ const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
4464
4819
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4465
4820
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4466
4821
  if (products.length === 0) {
@@ -4481,15 +4836,15 @@ async function syncPlans(options2 = {}) {
4481
4836
  ${existingResult.output.trim()}`);
4482
4837
  }
4483
4838
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4484
- const dir = mkdtempSync4(join24(tmpdir4(), "flare-sync-"));
4839
+ const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
4485
4840
  try {
4486
- const file = join24(dir, "sync-plans.sql");
4487
- writeFileSync17(file, renderSyncSql(rows));
4841
+ const file = join25(dir, "sync-plans.sql");
4842
+ writeFileSync18(file, renderSyncSql(rows));
4488
4843
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4489
4844
  if (result.code !== 0) throw new Error(`Could not write plans:
4490
4845
  ${result.output.trim()}`);
4491
4846
  } finally {
4492
- rmSync5(dir, { recursive: true, force: true });
4847
+ rmSync7(dir, { recursive: true, force: true });
4493
4848
  }
4494
4849
  let created = 0;
4495
4850
  for (const row of rows) {
@@ -4507,16 +4862,16 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
4507
4862
  }
4508
4863
 
4509
4864
  // src/commands/sync.ts
4510
- import { existsSync as existsSync23 } from "fs";
4511
- import { join as join25 } from "path";
4865
+ import { existsSync as existsSync24 } from "fs";
4866
+ import { join as join26 } from "path";
4512
4867
  import pc21 from "picocolors";
4513
4868
  async function syncTypes(options2 = {}) {
4514
4869
  const log2 = options2.log ?? ((message) => console.log(message));
4515
4870
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4516
4871
  const all = await loadResources(appRoot);
4517
4872
  const policies = await loadPolicies(appRoot);
4518
- const tracksPolicies = policies.length > 0 || existsSync23(join25(appRoot, "policies/index.ts"));
4519
- const plan = planFiles(all, tracksPolicies ? policies : void 0);
4873
+ const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
4874
+ const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4520
4875
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4521
4876
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
4522
4877
  logResults(results, log2);
@@ -4600,22 +4955,22 @@ ${result.output.trim()}`);
4600
4955
 
4601
4956
  // src/utils/install.ts
4602
4957
  import { spawn as spawn5 } from "child_process";
4603
- import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
4604
- import { join as join26 } from "path";
4958
+ import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
4959
+ import { join as join27 } from "path";
4605
4960
  function expectedPackages(dir) {
4606
- const npmLock = join26(dir, "package-lock.json");
4607
- if (existsSync24(npmLock)) {
4961
+ const npmLock = join27(dir, "package-lock.json");
4962
+ if (existsSync25(npmLock)) {
4608
4963
  try {
4609
- const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
4964
+ const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
4610
4965
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4611
4966
  } catch {
4612
4967
  return 0;
4613
4968
  }
4614
4969
  }
4615
- const pnpmLock = join26(dir, "pnpm-lock.yaml");
4616
- if (existsSync24(pnpmLock)) {
4970
+ const pnpmLock = join27(dir, "pnpm-lock.yaml");
4971
+ if (existsSync25(pnpmLock)) {
4617
4972
  try {
4618
- const text = readFileSync14(pnpmLock, "utf8");
4973
+ const text = readFileSync15(pnpmLock, "utf8");
4619
4974
  const packages = text.slice(text.indexOf("\npackages:"));
4620
4975
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4621
4976
  } catch {
@@ -4625,15 +4980,15 @@ function expectedPackages(dir) {
4625
4980
  return 0;
4626
4981
  }
4627
4982
  function installedPackages(dir) {
4628
- const root = join26(dir, "node_modules");
4629
- if (!existsSync24(root)) return 0;
4983
+ const root = join27(dir, "node_modules");
4984
+ if (!existsSync25(root)) return 0;
4630
4985
  let total = 0;
4631
4986
  try {
4632
4987
  for (const entry of readdirSync9(root, { withFileTypes: true })) {
4633
4988
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4634
4989
  if (entry.name.startsWith("@")) {
4635
4990
  try {
4636
- total += readdirSync9(join26(root, entry.name)).length;
4991
+ total += readdirSync9(join27(root, entry.name)).length;
4637
4992
  } catch {
4638
4993
  }
4639
4994
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -4677,31 +5032,33 @@ function install(packageManager, dir, onProgress) {
4677
5032
  // src/cli.ts
4678
5033
  function createCli() {
4679
5034
  const cli = cac("flare");
4680
- 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) => {
4681
- const wanted = !options2.skipInstall;
4682
- let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
4683
- if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
4684
- const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
4685
- let installed = false;
4686
- if (wanted) {
4687
- const spin = prompts3.spinner({ indicator: "timer" });
4688
- spin.start(`Installing dependencies with ${result.packageManager}`);
4689
- const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
4690
- spin.message(
4691
- percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
4692
- );
4693
- });
4694
- installed = outcome.code === 0;
4695
- if (installed) {
4696
- spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
4697
- } else {
4698
- spin.error(`${result.packageManager} install failed`);
4699
- process.stderr.write(outcome.output);
4700
- process.exitCode = 1;
5035
+ 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(
5036
+ async (dir, options2) => {
5037
+ const wanted = !options2.skipInstall;
5038
+ let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
5039
+ if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
5040
+ const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
5041
+ let installed = false;
5042
+ if (wanted) {
5043
+ const spin = prompts3.spinner({ indicator: "timer" });
5044
+ spin.start(`Installing dependencies with ${result.packageManager}`);
5045
+ const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
5046
+ spin.message(
5047
+ percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
5048
+ );
5049
+ });
5050
+ installed = outcome.code === 0;
5051
+ if (installed) {
5052
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
5053
+ } else {
5054
+ spin.error(`${result.packageManager} install failed`);
5055
+ process.stderr.write(outcome.output);
5056
+ process.exitCode = 1;
5057
+ }
4701
5058
  }
5059
+ printNextSteps(result, installed);
4702
5060
  }
4703
- printNextSteps(result, installed);
4704
- });
5061
+ );
4705
5062
  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(
4706
5063
  async (generator, name, second, options2) => {
4707
5064
  if (generator === "billing")