@flaredev/cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.js +685 -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,90 @@ 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
+ function readStack(appRoot) {
228
+ const file = join(appRoot, "package.json");
229
+ if (!existsSync(file)) return "cloudflare";
230
+ try {
231
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
232
+ return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
233
+ } catch {
234
+ return "cloudflare";
235
+ }
236
+ }
189
237
 
190
238
  // src/utils/fs.ts
191
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
192
- import { dirname, join, resolve } from "path";
239
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
240
+ import { dirname, join as join2, resolve } from "path";
193
241
  import { fileURLToPath } from "url";
194
242
  var RENAMES = { _gitignore: ".gitignore" };
195
243
  function copyTemplate(from, to, tokens) {
196
244
  mkdirSync(to, { recursive: true });
197
245
  let files = 0;
198
246
  for (const entry of readdirSync(from)) {
199
- const src = join(from, entry);
200
- const dest = join(to, RENAMES[entry] ?? entry);
247
+ const src = join2(from, entry);
248
+ const dest = join2(to, RENAMES[entry] ?? entry);
201
249
  if (statSync(src).isDirectory()) {
202
250
  files += copyTemplate(src, dest, tokens);
203
251
  continue;
204
252
  }
205
- const content = readFileSync(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
206
- writeFileSync(dest, content);
253
+ const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
254
+ writeFileSync2(dest, content);
207
255
  files++;
208
256
  }
209
257
  return files;
210
258
  }
211
259
  function writeJson(path, value) {
212
260
  mkdirSync(dirname(path), { recursive: true });
213
- writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
261
+ writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
214
262
  }
215
263
  function findUp(file, start) {
216
264
  let dir = resolve(start);
217
265
  while (true) {
218
- if (existsSync(join(dir, file))) return dir;
266
+ if (existsSync2(join2(dir, file))) return dir;
219
267
  const parent = dirname(dir);
220
268
  if (parent === dir) return void 0;
221
269
  dir = parent;
222
270
  }
223
271
  }
224
- var templatesDir = join(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
272
+ var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
225
273
 
226
274
  // src/utils/pm.ts
227
275
  import { spawnSync } from "child_process";
@@ -260,7 +308,7 @@ function toAppName(dir) {
260
308
  function flarePackageSpec(pkg, appDir, packageManager) {
261
309
  const repoPackage = resolve2(templatesDir, "../..", pkg);
262
310
  const repoRoot = resolve2(repoPackage, "../..");
263
- const fromCheckout = existsSync2(join2(repoPackage, "package.json")) && existsSync2(join2(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
311
+ const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
264
312
  if (!fromCheckout) return `^${FLARE_VERSION}`;
265
313
  const rel = relative(repoRoot, appDir);
266
314
  if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
@@ -270,32 +318,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
270
318
  function createApp(target, options2 = {}) {
271
319
  const dir = resolve2(target);
272
320
  const name = toAppName(dir);
273
- if (existsSync2(dir) && readdirSync2(dir).length > 0) {
321
+ if (existsSync3(dir) && readdirSync2(dir).length > 0) {
274
322
  throw new Error(`Directory ${dir} already exists and is not empty.`);
275
323
  }
276
324
  if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
277
325
  throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
278
326
  }
279
327
  const packageManager = options2.pm ?? detectPackageManager();
328
+ if (options2.stack !== void 0 && !isStack(options2.stack)) {
329
+ throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
330
+ }
331
+ const stack = options2.stack ?? "cloudflare";
280
332
  const authProviders = parseAuthProviders(options2.authProviders);
281
333
  const authMethods = parseAuthMethods(options2.auth);
282
334
  const theme = parseTheme(options2.theme);
283
335
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
284
336
  const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
285
- const files = copyTemplate(join2(templatesDir, "app"), dir, {
337
+ const files = copyTemplate(join3(templatesDir, "app"), dir, {
286
338
  APP_NAME: name,
287
339
  COMPAT_DATE: compatibilityDate,
288
340
  PM: packageManager,
289
341
  THEME: theme
290
342
  });
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"), {
343
+ if (stack === "next") {
344
+ copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
345
+ for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
346
+ }
347
+ writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
348
+ appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
349
+ const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
350
+ const uiOnly = Object.fromEntries(
351
+ Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
352
+ );
353
+ writeJson(join3(dir, "package.json"), {
294
354
  name,
295
355
  version: "0.1.0",
296
356
  private: true,
297
357
  type: "module",
298
- scripts: {
358
+ // Which stack this app targets. `flare gen` reads it; don't change it by hand.
359
+ flare: { stack },
360
+ scripts: stack === "next" ? {
361
+ dev: "next dev",
362
+ build: "prisma generate && next build",
363
+ start: "next start",
364
+ deploy: "vercel deploy --prod",
365
+ "db:generate": "prisma generate",
366
+ "db:migrate": "prisma migrate dev",
367
+ "db:studio": "prisma studio"
368
+ } : {
299
369
  dev: "flare dev",
300
370
  build: "flare build",
301
371
  start: "flare start",
@@ -304,18 +374,25 @@ function createApp(target, options2 = {}) {
304
374
  "db:generate": "drizzle-kit generate",
305
375
  "db:migrate:local": "wrangler d1 migrations apply DB --local"
306
376
  },
307
- dependencies: { "@flaredev/core": flarePackageSpec("core", dir, packageManager), ...APP_DEPENDENCIES },
308
- devDependencies: { "@flaredev/cli": flarePackageSpec("cli", dir, packageManager), ...APP_DEV_DEPENDENCIES }
377
+ dependencies: {
378
+ "@flaredev/core": flarePackageSpec("core", dir, packageManager),
379
+ ...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
380
+ },
381
+ devDependencies: {
382
+ "@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
383
+ ...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
384
+ }
309
385
  });
310
- writeFileSync2(
311
- join2(dir, ".dev.vars"),
386
+ writeFileSync3(
387
+ join3(dir, stack === "next" ? ".env" : ".dev.vars"),
312
388
  `BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
313
389
  RESEND_API_KEY=
314
390
  MAIL_FROM=
315
391
  ${devVarsEntries(authProviders)}`
316
392
  );
317
393
  if (packageManager === "pnpm" && !inWorkspace) {
318
- writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
394
+ const allowBuilds = stack === "next" ? "allowBuilds:\n '@prisma/engines': true\n prisma: true\n sharp: false\n" : "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n";
395
+ writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
319
396
  }
320
397
  const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
321
398
  if (pinned) copyLockfile(packageManager, dir);
@@ -326,9 +403,9 @@ ${devVarsEntries(authProviders)}`
326
403
  function copyLockfile(packageManager, dir) {
327
404
  const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
328
405
  if (!file) return false;
329
- const source = join2(templatesDir, "locks", file);
330
- if (!existsSync2(source)) return false;
331
- copyFileSync(source, join2(dir, file));
406
+ const source = join3(templatesDir, "locks", file);
407
+ if (!existsSync3(source)) return false;
408
+ copyFileSync(source, join3(dir, file));
332
409
  return true;
333
410
  }
334
411
  function printNextSteps(result, installed) {
@@ -348,8 +425,8 @@ function printNextSteps(result, installed) {
348
425
  }
349
426
 
350
427
  // 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";
428
+ import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
429
+ import { dirname as dirname6, join as join12 } from "path";
353
430
  import pc6 from "picocolors";
354
431
 
355
432
  // src/generator/grammar.ts
@@ -513,7 +590,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
513
590
 
514
591
  // src/generator/markers.ts
515
592
  import { createHash } from "crypto";
516
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
593
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
517
594
  import { dirname as dirname3 } from "path";
518
595
  var START = "// generated:start";
519
596
  var END = "// generated:end";
@@ -563,8 +640,8 @@ ${indentBlock(body, indent)}${indent}${END}
563
640
  ${after}`;
564
641
  }
565
642
  function inspectGenerated(path, content) {
566
- if (!existsSync3(path)) return "missing";
567
- const parts = splitMarkers(readFileSync2(path, "utf8"));
643
+ if (!existsSync4(path)) return "missing";
644
+ const parts = splitMarkers(readFileSync3(path, "utf8"));
568
645
  if (!parts) return "unmarked";
569
646
  if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
570
647
  if (normalize(parts.block) !== normalize(content)) return "outdated";
@@ -580,18 +657,18 @@ var DriftError = class extends Error {
580
657
  };
581
658
  function writeGenerated(path, content, options2 = {}) {
582
659
  const hash = hashBlock(content);
583
- if (!existsSync3(path)) {
660
+ if (!existsSync4(path)) {
584
661
  mkdirSync2(dirname3(path), { recursive: true });
585
- writeFileSync3(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
662
+ writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
586
663
  return "create";
587
664
  }
588
- const source = readFileSync2(path, "utf8");
665
+ const source = readFileSync3(path, "utf8");
589
666
  const parts = splitMarkers(source);
590
667
  if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
591
668
  if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
592
669
  const next = joinMarkers({ ...parts, block: content, hash });
593
670
  if (next === source) return "identical";
594
- writeFileSync3(path, next);
671
+ writeFileSync4(path, next);
595
672
  return "update";
596
673
  }
597
674
 
@@ -1346,17 +1423,17 @@ var billingDevVarsExampleEntries = () => [
1346
1423
  ].join("\n");
1347
1424
 
1348
1425
  // src/generator/load.ts
1349
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
1350
- import { join as join3, resolve as resolve3 } from "path";
1426
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
1427
+ import { join as join4, resolve as resolve3 } from "path";
1351
1428
  import { createJiti } from "jiti";
1352
1429
  async function loadResources(root) {
1353
1430
  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 });
1431
+ const dir = join4(appRoot, "resources");
1432
+ if (!existsSync5(dir)) return [];
1433
+ const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
1357
1434
  const loaded = [];
1358
1435
  for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
1359
- const path = join3(dir, file);
1436
+ const path = join4(dir, file);
1360
1437
  const mod = await jiti.import(path);
1361
1438
  const resource = mod.default;
1362
1439
  if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
@@ -1374,20 +1451,20 @@ async function loadResources(root) {
1374
1451
 
1375
1452
  // src/generator/migrations.ts
1376
1453
  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";
1454
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1455
+ import { join as join9 } from "path";
1379
1456
 
1380
1457
  // src/commands/run.ts
1381
1458
  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";
1459
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1460
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
1384
1461
  import pc4 from "picocolors";
1385
1462
 
1386
1463
  // src/tunnel.ts
1387
1464
  import { spawn, spawnSync as spawnSync2 } from "child_process";
1388
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
1465
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1389
1466
  import { homedir } from "os";
1390
- import { join as join4 } from "path";
1467
+ import { join as join5 } from "path";
1391
1468
  import pc2 from "picocolors";
1392
1469
  var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
1393
1470
  var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
@@ -1412,7 +1489,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
1412
1489
  throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
1413
1490
  }
1414
1491
  var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
1415
- var cacheDir = () => join4(homedir(), ".flare", "bin");
1492
+ var cacheDir = () => join5(homedir(), ".flare", "bin");
1416
1493
  function onPath() {
1417
1494
  const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1418
1495
  return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
@@ -1421,19 +1498,19 @@ async function download(log2) {
1421
1498
  const asset = releaseAsset();
1422
1499
  const dir = cacheDir();
1423
1500
  mkdirSync3(dir, { recursive: true });
1424
- const target = join4(dir, binName);
1501
+ const target = join5(dir, binName);
1425
1502
  log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1426
1503
  const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1427
1504
  if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1428
1505
  const bytes = Buffer.from(await response.arrayBuffer());
1429
1506
  const partial = `${target}.download`;
1430
1507
  if (asset.endsWith(".tgz")) {
1431
- writeFileSync4(`${partial}.tgz`, bytes);
1508
+ writeFileSync5(`${partial}.tgz`, bytes);
1432
1509
  const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
1433
- rmSync(`${partial}.tgz`, { force: true });
1510
+ rmSync2(`${partial}.tgz`, { force: true });
1434
1511
  if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
1435
1512
  } else {
1436
- writeFileSync4(partial, bytes);
1513
+ writeFileSync5(partial, bytes);
1437
1514
  renameSync(partial, target);
1438
1515
  }
1439
1516
  if (process.platform !== "win32") chmodSync(target, 493);
@@ -1442,8 +1519,8 @@ async function download(log2) {
1442
1519
  async function ensureCloudflared(log2 = console.log) {
1443
1520
  const found = onPath();
1444
1521
  if (found) return found;
1445
- const cached = join4(cacheDir(), binName);
1446
- if (existsSync5(cached)) return cached;
1522
+ const cached = join5(cacheDir(), binName);
1523
+ if (existsSync6(cached)) return cached;
1447
1524
  return download(log2);
1448
1525
  }
1449
1526
  async function openTunnel(localUrl, options2 = {}) {
@@ -1499,16 +1576,16 @@ function tunnelBanner(publicUrl, localUrl) {
1499
1576
 
1500
1577
  // src/commands/deploy.ts
1501
1578
  import { randomBytes as randomBytes2 } from "crypto";
1502
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1503
- import { join as join6 } from "path";
1579
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1580
+ import { join as join7 } from "path";
1504
1581
  import { createZoneClient } from "@flaredev/core/security";
1505
1582
  import { createJiti as createJiti2 } from "jiti";
1506
1583
  import pc3 from "picocolors";
1507
1584
 
1508
1585
  // src/utils/wrangler.ts
1509
1586
  import { spawn as spawn2 } from "child_process";
1510
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1511
- import { join as join5 } from "path";
1587
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1588
+ import { join as join6 } from "path";
1512
1589
  import { parse as parseJsonc } from "jsonc-parser";
1513
1590
  function runWrangler(wranglerBin, args, cwd, options2 = {}) {
1514
1591
  return new Promise((resolve6, reject) => {
@@ -1533,9 +1610,9 @@ ${output.trim()}`);
1533
1610
  return JSON.parse(output.slice(start, end + 1));
1534
1611
  }
1535
1612
  function readD1Databases(appRoot) {
1536
- const path = join5(appRoot, "wrangler.jsonc");
1537
- if (!existsSync6(path)) return [];
1538
- const config = parseJsonc(readFileSync3(path, "utf8"));
1613
+ const path = join6(appRoot, "wrangler.jsonc");
1614
+ if (!existsSync7(path)) return [];
1615
+ const config = parseJsonc(readFileSync4(path, "utf8"));
1539
1616
  return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
1540
1617
  }
1541
1618
 
@@ -1575,15 +1652,15 @@ function declaredSecretNames(devVarsExample) {
1575
1652
  return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
1576
1653
  }
1577
1654
  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 });
1655
+ const path = join7(appRoot, "security.config.ts");
1656
+ if (!existsSync8(path)) return null;
1657
+ const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1581
1658
  const mod = await jiti.import(path);
1582
1659
  if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
1583
1660
  return mod.default;
1584
1661
  }
1585
1662
  async function provisionZoneSecurity(appRoot, config, credentials, options2) {
1586
- const app = JSON.parse(readFileSync4(join6(appRoot, "package.json"), "utf8")).name ?? "app";
1663
+ const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
1587
1664
  const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
1588
1665
  if (config.zone?.rateLimits) {
1589
1666
  const result = await zone.syncRateLimits(config.zone.rateLimits);
@@ -1603,7 +1680,7 @@ async function runDeploy(args, context3) {
1603
1680
  const envArgs = plan.env ? ["--env", plan.env] : [];
1604
1681
  const step = (message) => log2(`
1605
1682
  ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1606
- const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1683
+ const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
1607
1684
  const migrateAfterDeploy = [];
1608
1685
  const migrate2 = async (db) => {
1609
1686
  step(`Applying migrations to ${db.database_name} (remote)`);
@@ -1662,8 +1739,8 @@ ${put.output.trim()}`);
1662
1739
  existing.add(name);
1663
1740
  }
1664
1741
  }
1665
- const examplePath = join6(appRoot, ".dev.vars.example");
1666
- const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1742
+ const examplePath = join7(appRoot, ".dev.vars.example");
1743
+ const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1667
1744
  if (optional.length > 0) {
1668
1745
  step("Optional secrets not set in production (features using them stay off):");
1669
1746
  for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
@@ -1698,25 +1775,30 @@ function findAppRoot(cwd) {
1698
1775
  while (dir) {
1699
1776
  const root = findUp("package.json", dir);
1700
1777
  if (!root) break;
1701
- const pkg = JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
1702
- if (pkg.dependencies?.vinext || pkg.devDependencies?.vinext) return root;
1778
+ const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
1779
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1780
+ if (deps["@flaredev/core"] || deps.vinext) return root;
1703
1781
  const parent = dirname4(root);
1704
1782
  dir = parent === root ? void 0 : parent;
1705
1783
  }
1706
- throw new Error("Not inside a Flare app (no package.json depending on vinext found). Run this from your app directory.");
1784
+ throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
1707
1785
  }
1708
1786
  function resolveBin(appRoot, pkgName, binName2) {
1709
- const pkgDir = findUp(join7("node_modules", pkgName, "package.json"), appRoot);
1787
+ const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
1710
1788
  if (!pkgDir) {
1711
1789
  throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
1712
1790
  }
1713
- const manifestPath = join7(pkgDir, "node_modules", pkgName, "package.json");
1714
- const { bin } = JSON.parse(readFileSync5(manifestPath, "utf8"));
1791
+ const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
1792
+ const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
1715
1793
  const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
1716
1794
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1717
- return join7(dirname4(manifestPath), relativeBin);
1795
+ return join8(dirname4(manifestPath), relativeBin);
1718
1796
  }
1719
1797
  function delegatedArgv(appRoot, command, forwarded) {
1798
+ if (readStack(appRoot) === "next") {
1799
+ const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
1800
+ throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
1801
+ }
1720
1802
  const spec = DELEGATED_COMMANDS[command];
1721
1803
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1722
1804
  }
@@ -1775,7 +1857,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
1775
1857
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1776
1858
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1777
1859
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1778
- if (command === "start" && !wantsHelp && !existsSync8(join7(appRoot, "dist/server/wrangler.json"))) {
1860
+ if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
1779
1861
  console.log("No production build found; running `flare build` first.\n");
1780
1862
  const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1781
1863
  if (code !== 0) return code;
@@ -1825,9 +1907,9 @@ function snapshotColumns(snapshot) {
1825
1907
  }
1826
1908
 
1827
1909
  // src/generator/migrations.ts
1828
- var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1910
+ var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1829
1911
  async function generateSchemaMigration(appRoot, name, options2 = {}) {
1830
- const dir = join8(appRoot, "migrations");
1912
+ const dir = join9(appRoot, "migrations");
1831
1913
  const before = new Set(listSql(dir));
1832
1914
  const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
1833
1915
  const { code, output } = await new Promise((resolve6, reject) => {
@@ -1855,10 +1937,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1855
1937
  for (const file of created) {
1856
1938
  const previous = previousSnapshot(dir, file);
1857
1939
  if (!previous) continue;
1858
- const path = join8(dir, file);
1859
- const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
1940
+ const path = join9(dir, file);
1941
+ const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
1860
1942
  if (changes.length) {
1861
- writeFileSync5(path, sql2);
1943
+ writeFileSync6(path, sql2);
1862
1944
  repairs.push(...changes.map((change) => `${file}: ${change}`));
1863
1945
  }
1864
1946
  }
@@ -1866,32 +1948,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1866
1948
  }
1867
1949
  function previousSnapshot(dir, migrationFile) {
1868
1950
  const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
1869
- const metaDir = join8(dir, "meta");
1870
- if (!prefix || !existsSync9(metaDir)) return void 0;
1951
+ const metaDir = join9(dir, "meta");
1952
+ if (!prefix || !existsSync10(metaDir)) return void 0;
1871
1953
  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;
1954
+ return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
1873
1955
  }
1874
1956
 
1875
1957
  // 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";
1958
+ import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
1959
+ import { dirname as dirname5, join as join11 } from "path";
1878
1960
  import pc5 from "picocolors";
1879
1961
 
1880
1962
  // src/generator/policy.ts
1881
- import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1882
- import { join as join9, resolve as resolve5 } from "path";
1963
+ import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
1964
+ import { join as join10, resolve as resolve5 } from "path";
1883
1965
  import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
1884
1966
  import { createJiti as createJiti3 } from "jiti";
1885
1967
  var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
1886
1968
  var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
1887
1969
  async function loadPolicies(root) {
1888
1970
  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 });
1971
+ const dir = join10(appRoot, "policies");
1972
+ if (!existsSync11(dir)) return [];
1973
+ const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1892
1974
  const loaded = [];
1893
1975
  for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
1894
- const mod = await jiti.import(join9(dir, file));
1976
+ const mod = await jiti.import(join10(dir, file));
1895
1977
  const policy = mod.default;
1896
1978
  if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
1897
1979
  throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
@@ -1936,15 +2018,136 @@ function renderPolicyRegistry(all) {
1936
2018
  ].join("\n");
1937
2019
  }
1938
2020
 
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);
2021
+ // src/generator/prisma.ts
2022
+ import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
1943
2023
  var q = (value) => JSON.stringify(value);
2024
+ var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
2025
+ var enumMember = (option) => {
2026
+ const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
2027
+ return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
2028
+ };
2029
+ function prismaType(resource, key, def) {
2030
+ switch (def.kind) {
2031
+ case "int":
2032
+ return "Int";
2033
+ case "float":
2034
+ return "Float";
2035
+ case "boolean":
2036
+ return "Boolean";
2037
+ case "date":
2038
+ case "datetime":
2039
+ return "DateTime";
2040
+ case "enum":
2041
+ return enumTypeName(resource.name, key);
2042
+ case "multiselect":
2043
+ return "String[]";
2044
+ default:
2045
+ return "String";
2046
+ }
2047
+ }
2048
+ function attributes(key, def) {
2049
+ const parts = [];
2050
+ const column = columnName(key);
2051
+ if (column !== key) parts.push(`@map(${q(column)})`);
2052
+ if ("unique" in def && def.unique) parts.push("@unique");
2053
+ if (def.kind === "text") parts.push("@db.Text");
2054
+ if (def.kind === "date") parts.push("@db.Date");
2055
+ if ("default" in def && def.default !== void 0) {
2056
+ const value = def.default;
2057
+ if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
2058
+ else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
2059
+ else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
2060
+ }
2061
+ return parts;
2062
+ }
2063
+ var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
2064
+ function renderPrismaEnums(resource) {
2065
+ return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
2066
+ if (def.kind !== "enum") return "";
2067
+ return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
2068
+ }).filter(Boolean);
2069
+ }
2070
+ function backReferences(target, all) {
2071
+ const found = [];
2072
+ for (const { resource: source } of all) {
2073
+ const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
2074
+ const ambiguous = pointing.length > 1;
2075
+ for (const [key, def] of pointing) {
2076
+ if (def.kind !== "belongsTo") continue;
2077
+ const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
2078
+ const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
2079
+ found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
2080
+ }
2081
+ }
2082
+ return found;
2083
+ }
2084
+ function renderPrismaModel(entry, all) {
2085
+ const resource = entry.resource;
2086
+ const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
2087
+ const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
2088
+ const indexes = [];
2089
+ for (const [key, def] of Object.entries(resource.fields)) {
2090
+ if (def.kind === "hasMany") {
2091
+ if (!byName.get(def.target)) {
2092
+ throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2093
+ }
2094
+ continue;
2095
+ }
2096
+ if (def.kind === "belongsTo") {
2097
+ const target = byName.get(def.target);
2098
+ if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2099
+ const optional2 = def.required ? "" : "?";
2100
+ const relationField = key.replace(/Id$/, "");
2101
+ const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
2102
+ const relationArgs = [
2103
+ ...named?.relationName ? [q(named.relationName)] : [],
2104
+ `fields: [${key}]`,
2105
+ "references: [id]",
2106
+ `onDelete: ${onDeleteAction(def.onDelete)}`
2107
+ ];
2108
+ lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
2109
+ lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
2110
+ indexes.push(` @@index([${key}])`);
2111
+ continue;
2112
+ }
2113
+ const optional = def.required || def.kind === "multiselect" ? "" : "?";
2114
+ lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
2115
+ }
2116
+ for (const back of backReferences(resource, all)) {
2117
+ const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
2118
+ lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
2119
+ }
2120
+ lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
2121
+ lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
2122
+ indexes.push(` @@index([createdAt])`);
2123
+ const sortField = resource.defaultSort.field;
2124
+ if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
2125
+ for (const [key, def] of storedFields(resource)) {
2126
+ if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
2127
+ }
2128
+ lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
2129
+ return lines.join("\n");
2130
+ }
2131
+ function renderPrismaSchema(all) {
2132
+ const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2133
+ const blocks = [];
2134
+ for (const entry of ordered) {
2135
+ blocks.push(...renderPrismaEnums(entry.resource));
2136
+ blocks.push(renderPrismaModel(entry, all));
2137
+ }
2138
+ return `${blocks.join("\n\n")}
2139
+ `;
2140
+ }
2141
+
2142
+ // src/generator/render.ts
2143
+ import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
2144
+ var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
2145
+ var tableExport = (resource) => camelCase4(resource.table);
2146
+ var q2 = (value) => JSON.stringify(value);
1944
2147
  var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
1945
2148
  var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
1946
2149
  function columnExpression(key, def, resources, selfName) {
1947
- const col = q(columnName(key));
2150
+ const col = q2(columnName2(key));
1948
2151
  let expr;
1949
2152
  switch (def.kind) {
1950
2153
  case "int":
@@ -1957,10 +2160,10 @@ function columnExpression(key, def, resources, selfName) {
1957
2160
  expr = `integer(${col}, { mode: "boolean" })`;
1958
2161
  break;
1959
2162
  case "enum":
1960
- expr = `text(${col}, { enum: ${q(def.options)} })`;
2163
+ expr = `text(${col}, { enum: ${q2(def.options)} })`;
1961
2164
  break;
1962
2165
  case "multiselect":
1963
- expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q(option)).join(" | ")})[]>()`;
2166
+ expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
1964
2167
  break;
1965
2168
  case "belongsTo": {
1966
2169
  const target = resources.get(def.target);
@@ -1968,7 +2171,7 @@ function columnExpression(key, def, resources, selfName) {
1968
2171
  throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
1969
2172
  }
1970
2173
  const onDelete = def.onDelete ?? "restrict";
1971
- expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q(onDelete)} })`;
2174
+ expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
1972
2175
  break;
1973
2176
  }
1974
2177
  default:
@@ -1976,13 +2179,13 @@ function columnExpression(key, def, resources, selfName) {
1976
2179
  }
1977
2180
  if (def.required) expr += ".notNull()";
1978
2181
  if ("unique" in def && def.unique) expr += ".unique()";
1979
- if ("default" in def && def.default !== void 0) expr += `.default(${q(def.default)})`;
2182
+ if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
1980
2183
  return expr;
1981
2184
  }
1982
2185
  function renderTableModule(entry, all) {
1983
2186
  const { resource } = entry;
1984
2187
  const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
1985
- const fields = storedFields(resource);
2188
+ const fields = storedFields2(resource);
1986
2189
  const name = tableExport(resource);
1987
2190
  const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
1988
2191
  const extras = [];
@@ -1990,12 +2193,12 @@ function renderTableModule(entry, all) {
1990
2193
  const addIndex = (key, column) => {
1991
2194
  if (indexed.has(key)) return;
1992
2195
  indexed.add(key);
1993
- extras.push(` index(${q(`${resource.table}_${columnName(column)}_idx`)}).on(table.${key}),`);
2196
+ extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
1994
2197
  };
1995
2198
  for (const [key, def] of fields) {
1996
2199
  if (def.kind === "enum") {
1997
2200
  const options2 = def.options.map(sqlString).join(", ");
1998
- extras.push(` check(${q(`${resource.table}_${columnName(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
2201
+ extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
1999
2202
  if (def.filterable !== false) addIndex(key, key);
2000
2203
  }
2001
2204
  if (def.kind === "belongsTo") addIndex(key, key);
@@ -2017,7 +2220,7 @@ function renderTableModule(entry, all) {
2017
2220
  ...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
2018
2221
  "",
2019
2222
  `export const ${name} = sqliteTable(`,
2020
- ` ${q(resource.table)},`,
2223
+ ` ${q2(resource.table)},`,
2021
2224
  " {",
2022
2225
  ` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
2023
2226
  ...columns.map(
@@ -2057,12 +2260,12 @@ function renderRelations(all) {
2057
2260
  const target = byName.get(relation.target);
2058
2261
  used.add(target);
2059
2262
  const other = tableExport(target);
2060
- return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q(relation.relationName)} }),`;
2263
+ return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
2061
2264
  }),
2062
2265
  ...hasMany.map((relation) => {
2063
2266
  const target = byName.get(relation.target);
2064
2267
  used.add(target);
2065
- return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q(relation.relationName)} }),`;
2268
+ return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
2066
2269
  })
2067
2270
  ];
2068
2271
  const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
@@ -2075,32 +2278,40 @@ ${lines.join("\n")}
2075
2278
  const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
2076
2279
  return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
2077
2280
  }
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(
2281
+ var handlerImports = (entry, stack = "cloudflare") => {
2282
+ const local = resourceLocal(entry.stem);
2283
+ const source = stack === "next" ? {
2284
+ imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
2285
+ wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2286
+ } : {
2287
+ imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2288
+ wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
2289
+ };
2290
+ return [
2291
+ `import { createResourceHandlers } from "@flaredev/core/server";`,
2292
+ ...source.imports,
2293
+ `import { authorize, currentUser } from "@/lib/api";`,
2294
+ `import { revalidateResource } from "@/lib/cache";`,
2295
+ `import ${local} from "@/resources/${entry.stem}.resource";`,
2296
+ "",
2297
+ `const handlers = createResourceHandlers({`,
2298
+ ` resource: ${local},`,
2299
+ ...source.wiring,
2300
+ ` authorize,`,
2301
+ ` currentUser,`,
2302
+ ` onChange: revalidateResource,`,
2303
+ `});`,
2304
+ ""
2305
+ ];
2306
+ };
2307
+ function renderCollectionRoute(entry, stack = "cloudflare") {
2308
+ return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
2098
2309
  "\n"
2099
2310
  );
2100
2311
  }
2101
- function renderItemRoute(entry) {
2312
+ function renderItemRoute(entry, stack = "cloudflare") {
2102
2313
  return [
2103
- ...handlerImports(entry),
2314
+ ...handlerImports(entry, stack),
2104
2315
  "export const GET = handlers.item.GET;",
2105
2316
  "export const PATCH = handlers.item.PATCH;",
2106
2317
  "export const PUT = handlers.item.PUT;",
@@ -2179,7 +2390,7 @@ function renderClient(entry) {
2179
2390
  `import { createResourceClient } from "@flaredev/core/client";`,
2180
2391
  `import type ${local} from "./${stem}.resource";`,
2181
2392
  "",
2182
- `export const ${camelCase3(stem)}Client = createResourceClient<typeof ${local}>(${q(`/api/${resource.slug}`)});`,
2393
+ `export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
2183
2394
  "",
2184
2395
  `export type ${resource.name} = typeof ${local}.$types.record;`,
2185
2396
  `export type ${resource.name}Create = typeof ${local}.$types.create;`,
@@ -2193,7 +2404,7 @@ function renderValidators(entry) {
2193
2404
  `import { createValidators } from "@flaredev/core";`,
2194
2405
  `import ${local} from "./${entry.stem}.resource";`,
2195
2406
  "",
2196
- `export const ${camelCase3(entry.stem)}Validators = createValidators(${local});`,
2407
+ `export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
2197
2408
  ""
2198
2409
  ].join("\n");
2199
2410
  }
@@ -2223,12 +2434,31 @@ function renderServerRegistry(all) {
2223
2434
  ""
2224
2435
  ].join("\n");
2225
2436
  }
2226
- function resourceFiles(entry, all) {
2437
+ function renderPrismaServerRegistry(all) {
2438
+ if (all.length === 0) return "export const resourceTables = {} as const;\n";
2439
+ const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
2440
+ return [
2441
+ `import { prisma } from "@/lib/db";`,
2442
+ `import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
2443
+ "",
2444
+ "export const resourceTables = {",
2445
+ ...sorted.map(
2446
+ ({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
2447
+ ),
2448
+ "} as const;",
2449
+ "",
2450
+ "export type ResourceName = keyof typeof resourceTables;",
2451
+ ""
2452
+ ].join("\n");
2453
+ }
2454
+ function resourceFiles(entry, all, stack = "cloudflare") {
2227
2455
  const { resource, stem } = entry;
2228
2456
  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) },
2457
+ // The schema is the one file that belongs to a stack. On Next.js every model lives
2458
+ // in one prisma/schema.prisma, written by planFiles rather than per resource.
2459
+ ...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
2460
+ { path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
2461
+ { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
2232
2462
  { path: `resources/${stem}.client.ts`, content: renderClient(entry) },
2233
2463
  { path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
2234
2464
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
@@ -2243,21 +2473,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
2243
2473
  // rewritten on regeneration; add your own code outside it.
2244
2474
  `;
2245
2475
  var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
2246
- function planFiles(all, policies) {
2476
+ function planFiles(all, policies, stack = "cloudflare") {
2247
2477
  const files = [];
2248
2478
  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
- );
2479
+ for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
2480
+ }
2481
+ files.push({
2482
+ path: "resources/index.ts",
2483
+ content: renderRegistry(all),
2484
+ header: "// Registry of every resource descriptor (maintained by flare gen).\n"
2485
+ });
2486
+ if (stack === "next") {
2487
+ files.push(
2488
+ // Prisma keeps every model in one schema and writes its own migrations, so there
2489
+ // is no per-table module and no relations file to maintain.
2490
+ {
2491
+ path: "prisma/schema/resources.prisma",
2492
+ content: renderPrismaSchema(all),
2493
+ header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
2494
+ },
2495
+ {
2496
+ path: "resources/server.ts",
2497
+ content: renderPrismaServerRegistry(all),
2498
+ header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
2499
+ }
2500
+ );
2501
+ } else {
2502
+ files.push(
2503
+ { path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
2504
+ {
2505
+ path: "resources/server.ts",
2506
+ content: renderServerRegistry(all),
2507
+ header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
2508
+ },
2509
+ { path: "db/schema.ts", content: renderSchemaIndex(all) }
2510
+ );
2511
+ }
2261
2512
  if (policies) {
2262
2513
  files.push({
2263
2514
  path: "policies/index.ts",
@@ -2269,7 +2520,7 @@ function planFiles(all, policies) {
2269
2520
  }
2270
2521
  function applyPlan(appRoot, plan, options2 = {}) {
2271
2522
  return plan.map(({ path, content, header }) => {
2272
- const absolute = join10(appRoot, path);
2523
+ const absolute = join11(appRoot, path);
2273
2524
  const status = inspectGenerated(absolute, content);
2274
2525
  if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
2275
2526
  if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
@@ -2288,30 +2539,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
2288
2539
  function findOrphans(appRoot, all) {
2289
2540
  const planned = new Set(planFiles(all).map((file) => file.path));
2290
2541
  const candidates = [];
2291
- const list2 = (dir, match) => existsSync11(join10(appRoot, dir)) ? readdirSync6(join10(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2542
+ const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2292
2543
  candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
2293
2544
  candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
2294
2545
  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);
2546
+ for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2296
2547
  }
2297
2548
  for (const dir of list2("app/dashboard", () => true)) {
2298
2549
  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);
2550
+ if (existsSync12(join11(appRoot, file))) candidates.push(file);
2300
2551
  }
2301
2552
  }
2302
2553
  const orphans = [];
2303
2554
  for (const path of candidates) {
2304
- const match = HEADER_PATTERN.exec(readFileSync7(join10(appRoot, path), "utf8"));
2555
+ const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
2305
2556
  if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
2306
2557
  }
2307
2558
  return orphans;
2308
2559
  }
2309
2560
  function ensureSupportFiles(appRoot, log2) {
2310
2561
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2311
- const target = join10(appRoot, path);
2312
- if (existsSync11(target)) continue;
2562
+ const target = join11(appRoot, path);
2563
+ if (existsSync12(target)) continue;
2313
2564
  mkdirSync4(dirname5(target), { recursive: true });
2314
- copyFileSync2(join10(templatesDir, "app", path), target);
2565
+ copyFileSync2(join11(templatesDir, "app", path), target);
2315
2566
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2316
2567
  }
2317
2568
  }
@@ -2339,15 +2590,15 @@ function billingFiles(appName) {
2339
2590
  }
2340
2591
  var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
2341
2592
  function appendVars(appRoot, target) {
2342
- const path = join11(appRoot, target);
2343
- const text = existsSync12(path) ? readFileSync8(path, "utf8") : "";
2593
+ const path = join12(appRoot, target);
2594
+ const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
2344
2595
  if (/STRIPE_SECRET_KEY/.test(text)) return false;
2345
2596
  appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
2346
2597
  return true;
2347
2598
  }
2348
2599
  function addStripeDependency(appRoot) {
2349
- const path = join11(appRoot, "package.json");
2350
- const pkg = JSON.parse(readFileSync8(path, "utf8"));
2600
+ const path = join12(appRoot, "package.json");
2601
+ const pkg = JSON.parse(readFileSync9(path, "utf8"));
2351
2602
  pkg.dependencies ??= {};
2352
2603
  if (pkg.dependencies.stripe) return false;
2353
2604
  pkg.dependencies.stripe = STRIPE_DEP;
@@ -2356,14 +2607,14 @@ function addStripeDependency(appRoot) {
2356
2607
  }
2357
2608
  function writeBillingResource(appRoot, name, force, log2) {
2358
2609
  const relative2 = descriptorPath(name);
2359
- const path = join11(appRoot, relative2);
2360
- if (!existsSync12(path)) {
2610
+ const path = join12(appRoot, relative2);
2611
+ if (!existsSync13(path)) {
2361
2612
  mkdirSync5(dirname6(path), { recursive: true });
2362
- writeFileSync6(path, renderBillingDescriptor(name));
2613
+ writeFileSync7(path, renderBillingDescriptor(name));
2363
2614
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2364
2615
  return;
2365
2616
  }
2366
- const parts = splitMarkers(readFileSync8(path, "utf8"));
2617
+ const parts = splitMarkers(readFileSync9(path, "utf8"));
2367
2618
  if (!parts) throw new Error(`${relative2} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
2368
2619
  const merged = mergeBillingFields(name, parts.block);
2369
2620
  if (!merged) {
@@ -2387,26 +2638,26 @@ async function genBilling(options2 = {}) {
2387
2638
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2388
2639
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2389
2640
  const relative2 = policyPath(name);
2390
- if (existsSync12(join11(appRoot, relative2))) {
2641
+ if (existsSync13(join12(appRoot, relative2))) {
2391
2642
  log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2392
2643
  continue;
2393
2644
  }
2394
- mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2395
- writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2645
+ mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2646
+ writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2396
2647
  log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2397
2648
  }
2398
2649
  const files = [];
2399
- const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2650
+ const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2400
2651
  for (const { path, content } of billingFiles(appName)) {
2401
- const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2652
+ const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
2402
2653
  files.push({ path, status: "missing", outcome });
2403
2654
  const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2404
2655
  log2(`${label.padEnd(18)} ${path}`);
2405
2656
  }
2406
2657
  for (const path of RETIRED_FILES) {
2407
- const absolute = join11(appRoot, path);
2408
- if (!existsSync12(absolute)) continue;
2409
- const parts = splitMarkers(readFileSync8(absolute, "utf8"));
2658
+ const absolute = join12(appRoot, path);
2659
+ if (!existsSync13(absolute)) continue;
2660
+ const parts = splitMarkers(readFileSync9(absolute, "utf8"));
2410
2661
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2411
2662
  if (untouched) {
2412
2663
  unlinkSync(absolute);
@@ -2427,7 +2678,7 @@ async function genBilling(options2 = {}) {
2427
2678
  }
2428
2679
  const all = await loadResources(appRoot);
2429
2680
  ensureSupportFiles(appRoot, log2);
2430
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2681
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
2431
2682
  let migrations = [];
2432
2683
  if (!options2.skipMigration) {
2433
2684
  const result = await generateSchemaMigration(appRoot, "billing");
@@ -2450,8 +2701,8 @@ async function genBilling(options2 = {}) {
2450
2701
  }
2451
2702
 
2452
2703
  // 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";
2704
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
2705
+ import { dirname as dirname7, join as join13 } from "path";
2455
2706
  import pc7 from "picocolors";
2456
2707
 
2457
2708
  // src/generator/security.ts
@@ -2979,28 +3230,28 @@ function securityFiles(appName) {
2979
3230
  }
2980
3231
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2981
3232
  function writeOnce(appRoot, relative2, content, log2) {
2982
- const path = join12(appRoot, relative2);
2983
- if (existsSync13(path)) {
3233
+ const path = join13(appRoot, relative2);
3234
+ if (existsSync14(path)) {
2984
3235
  log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2985
3236
  return;
2986
3237
  }
2987
3238
  mkdirSync6(dirname7(path), { recursive: true });
2988
- writeFileSync7(path, content);
3239
+ writeFileSync8(path, content);
2989
3240
  log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
2990
3241
  }
2991
3242
  async function genSecurity(options2 = {}) {
2992
3243
  const log2 = options2.log ?? ((message) => console.log(message));
2993
3244
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2994
- const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
3245
+ const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
2995
3246
  writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
2996
3247
  writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
2997
3248
  writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
2998
3249
  const files = [];
2999
3250
  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()));
3251
+ const absolute = join13(appRoot, path);
3252
+ if (path === "lib/security.ts" && existsSync14(absolute)) {
3253
+ const source = readFileSync10(absolute, "utf8");
3254
+ if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
3004
3255
  }
3005
3256
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
3006
3257
  files.push({ path, status: "missing", outcome });
@@ -3008,16 +3259,16 @@ async function genSecurity(options2 = {}) {
3008
3259
  log2(`${label.padEnd(18)} ${path}`);
3009
3260
  };
3010
3261
  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());
3262
+ const navPath = join13(appRoot, "lib/dashboard-nav.ts");
3263
+ if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
3013
3264
  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");
3265
+ const wranglerPath = join13(appRoot, "wrangler.jsonc");
3015
3266
  let bindings = [];
3016
- if (existsSync13(wranglerPath)) {
3017
- const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
3267
+ if (existsSync14(wranglerPath)) {
3268
+ const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
3018
3269
  bindings = added;
3019
3270
  if (added.length) {
3020
- writeFileSync7(wranglerPath, text);
3271
+ writeFileSync8(wranglerPath, text);
3021
3272
  for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
3022
3273
  }
3023
3274
  } else {
@@ -3025,7 +3276,7 @@ async function genSecurity(options2 = {}) {
3025
3276
  }
3026
3277
  const all = await loadResources(appRoot);
3027
3278
  ensureSupportFiles(appRoot, log2);
3028
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
3279
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
3029
3280
  let migrations = [];
3030
3281
  if (!options2.skipMigration) {
3031
3282
  const result = await generateSchemaMigration(appRoot, "security");
@@ -3048,15 +3299,15 @@ async function genSecurity(options2 = {}) {
3048
3299
  }
3049
3300
 
3050
3301
  // src/commands/theme.ts
3051
- import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3052
- import { join as join13 } from "path";
3302
+ import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3303
+ import { join as join14 } from "path";
3053
3304
  import pc8 from "picocolors";
3054
3305
  var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
3055
3306
  function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3056
3307
  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");
3308
+ const path = join14(appRoot, "lib/site.ts");
3309
+ if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
3310
+ const source = readFileSync11(path, "utf8");
3060
3311
  const current = THEME_LINE.exec(source)?.[1];
3061
3312
  if (!name) {
3062
3313
  for (const [theme2, description] of Object.entries(THEMES)) {
@@ -3067,7 +3318,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
3067
3318
  }
3068
3319
  const theme = parseTheme(name);
3069
3320
  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,`));
3321
+ writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3071
3322
  log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3072
3323
  }
3073
3324
 
@@ -3139,9 +3390,9 @@ async function askCreateQuestions(given, appName) {
3139
3390
  }
3140
3391
 
3141
3392
  // 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";
3393
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
3394
+ import { dirname as dirname8, join as join15 } from "path";
3395
+ import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3145
3396
  import pc10 from "picocolors";
3146
3397
  function validateFields(name, fields, others, log2) {
3147
3398
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
@@ -3153,7 +3404,7 @@ function validateFields(name, fields, others, log2) {
3153
3404
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
3154
3405
  const graph = relationGraph2([...others, candidate]);
3155
3406
  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).`));
3407
+ log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
3157
3408
  }
3158
3409
  }
3159
3410
  async function genResource(rawName, options2) {
@@ -3161,8 +3412,8 @@ async function genResource(rawName, options2) {
3161
3412
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3162
3413
  const name = resourceName(rawName);
3163
3414
  const relativePath = descriptorPath(name);
3164
- const path = join14(appRoot, relativePath);
3165
- const exists = existsSync15(path);
3415
+ const path = join15(appRoot, relativePath);
3416
+ const exists = existsSync16(path);
3166
3417
  if (!exists && !options2.fields) {
3167
3418
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
3168
3419
  }
@@ -3173,7 +3424,7 @@ async function genResource(rawName, options2) {
3173
3424
  validateFields(name, fields, others, log2);
3174
3425
  if (!exists) {
3175
3426
  mkdirSync7(dirname8(path), { recursive: true });
3176
- writeFileSync9(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3427
+ writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3177
3428
  log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
3178
3429
  } else {
3179
3430
  try {
@@ -3195,7 +3446,7 @@ async function genResource(rawName, options2) {
3195
3446
  const all = await loadResources(appRoot);
3196
3447
  table = all.find(({ resource }) => resource.name === name).resource.table;
3197
3448
  ensureSupportFiles(appRoot, log2);
3198
- written = applyPlan(appRoot, planFiles(all), { force: options2.force });
3449
+ written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
3199
3450
  logResults(written, log2);
3200
3451
  } catch (error) {
3201
3452
  const hint = exists ? "" : `
@@ -3209,22 +3460,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
3209
3460
  }
3210
3461
  let migrations = [];
3211
3462
  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(`
3463
+ if (readStack(appRoot) === "next") {
3464
+ log2(`
3465
+ Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
3466
+ } else {
3467
+ const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3468
+ migrations = result.files;
3469
+ for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3470
+ for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3471
+ if (migrations.length) log2(`
3217
3472
  Next: ${pc10.bold("flare migrate")} to apply it locally.`);
3218
- else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3473
+ else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3474
+ }
3219
3475
  }
3220
3476
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
3221
3477
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
3222
3478
  }
3223
3479
 
3224
3480
  // 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";
3481
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
3482
+ import { join as join16 } from "path";
3483
+ import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
3228
3484
  import pc11 from "picocolors";
3229
3485
  var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
3230
3486
  var ACTIONS = ["read", "create", "update", "delete"];
@@ -3236,7 +3492,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3236
3492
  const name = kebabCase3(rawName);
3237
3493
  if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
3238
3494
  const resources = await loadResources(appRoot);
3239
- const wanted = pascalCase2(resourceName2);
3495
+ const wanted = pascalCase3(resourceName2);
3240
3496
  const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
3241
3497
  if (!entry) {
3242
3498
  throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
@@ -3245,11 +3501,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3245
3501
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3246
3502
  const { resource, stem } = entry;
3247
3503
  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 });
3504
+ const path = join16(appRoot, relative2);
3505
+ if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
3506
+ mkdirSync8(join16(path, ".."), { recursive: true });
3251
3507
  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 }));
3508
+ writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3253
3509
  log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3254
3510
  log2(`
3255
3511
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
@@ -3285,8 +3541,8 @@ ${body}
3285
3541
  }
3286
3542
 
3287
3543
  // 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";
3544
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
3545
+ import { basename as basename2, join as join17 } from "path";
3290
3546
  import { snakeCase } from "@flaredev/core";
3291
3547
  import pc12 from "picocolors";
3292
3548
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
@@ -3307,12 +3563,12 @@ async function genMigration(rawName, options2 = {}) {
3307
3563
  for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
3308
3564
  for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
3309
3565
  if (!options2.fromSchema) {
3310
- const downDir = join16(appRoot, "migrations", "down");
3566
+ const downDir = join17(appRoot, "migrations", "down");
3311
3567
  mkdirSync9(downDir, { recursive: true });
3312
3568
  for (const file of files) {
3313
- const down = join16(downDir, basename2(file));
3314
- if (existsSync17(down)) continue;
3315
- writeFileSync11(down, DOWN_TEMPLATE(basename2(file)));
3569
+ const down = join17(downDir, basename2(file));
3570
+ if (existsSync18(down)) continue;
3571
+ writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
3316
3572
  log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3317
3573
  }
3318
3574
  log2(`
@@ -3325,8 +3581,8 @@ Next: ${pc12.bold("flare migrate")}.`);
3325
3581
  }
3326
3582
 
3327
3583
  // 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";
3584
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
3585
+ import { dirname as dirname9, join as join18 } from "path";
3330
3586
  import { definePolicy } from "@flaredev/core";
3331
3587
  import pc13 from "picocolors";
3332
3588
  function parseRoles(input, what = "--roles") {
@@ -3344,8 +3600,8 @@ async function genPolicy(rawName, options2) {
3344
3600
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3345
3601
  }
3346
3602
  const relative2 = policyPath(name);
3347
- const path = join17(appRoot, relative2);
3348
- const exists = existsSync18(path);
3603
+ const path = join18(appRoot, relative2);
3604
+ const exists = existsSync19(path);
3349
3605
  let roles;
3350
3606
  let deleteRoles;
3351
3607
  if (options2.roles || !exists) {
@@ -3357,7 +3613,7 @@ async function genPolicy(rawName, options2) {
3357
3613
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
3358
3614
  if (!exists) {
3359
3615
  mkdirSync10(dirname9(path), { recursive: true });
3360
- writeFileSync12(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3616
+ writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3361
3617
  log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3362
3618
  } else {
3363
3619
  try {
@@ -3372,16 +3628,16 @@ async function genPolicy(rawName, options2) {
3372
3628
  }
3373
3629
  }
3374
3630
  const policies = await loadPolicies(appRoot);
3375
- logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
3631
+ logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3376
3632
  log2(`
3377
3633
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3378
3634
  return { name, path: relative2 };
3379
3635
  }
3380
3636
 
3381
3637
  // src/commands/migrate.ts
3382
- import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
3638
+ import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
3383
3639
  import { tmpdir } from "os";
3384
- import { join as join18 } from "path";
3640
+ import { join as join19 } from "path";
3385
3641
  import pc14 from "picocolors";
3386
3642
 
3387
3643
  // src/generator/down.ts
@@ -3475,7 +3731,17 @@ function context(options2) {
3475
3731
  };
3476
3732
  }
3477
3733
  async function migrate(options2 = {}) {
3478
- const { appRoot, db, target, wrangler } = context(options2);
3734
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3735
+ const log2 = options2.log ?? ((message) => console.log(message));
3736
+ if (readStack(appRoot) === "next") {
3737
+ if (options2.remote) {
3738
+ log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
3739
+ }
3740
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
3741
+ log2(`${pc14.cyan("prisma")} migrate deploy`);
3742
+ return runNode([prisma, "migrate", "deploy"], appRoot);
3743
+ }
3744
+ const { db, target, wrangler } = context(options2);
3479
3745
  const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
3480
3746
  return result.code;
3481
3747
  }
@@ -3499,7 +3765,7 @@ async function rollback(options2 = {}) {
3499
3765
  const steps = options2.steps ?? 1;
3500
3766
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3501
3767
  const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
3502
- const migrationsDir = join18(appRoot, db.migrations_dir ?? "migrations");
3768
+ const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
3503
3769
  const applied = await appliedMigrations(options2, steps);
3504
3770
  if (applied.length === 0) {
3505
3771
  log2("Nothing to roll back.");
@@ -3508,18 +3774,18 @@ async function rollback(options2 = {}) {
3508
3774
  const plan = [];
3509
3775
  for (const { name } of applied) {
3510
3776
  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");
3777
+ const handWritten = join19(migrationsDir, "down", name);
3778
+ if (existsSync20(handWritten)) {
3779
+ const sql2 = readFileSync12(handWritten, "utf8");
3514
3780
  if (splitStatements(sql2).length === 0) {
3515
3781
  throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
3516
3782
  }
3517
3783
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3518
3784
  continue;
3519
3785
  }
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"));
3786
+ const upPath = join19(migrationsDir, name);
3787
+ if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3788
+ const down = deriveDown(readFileSync12(upPath, "utf8"));
3523
3789
  if (!down.reversible) {
3524
3790
  throw new Error(
3525
3791
  `Can't roll back ${name}: ${down.reason}
@@ -3539,15 +3805,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3539
3805
  }
3540
3806
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3541
3807
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3542
- const dir = mkdtempSync(join18(tmpdir(), "flare-rollback-"));
3808
+ const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
3543
3809
  try {
3544
- const file = join18(dir, "rollback.sql");
3545
- writeFileSync13(file, script);
3810
+ const file = join19(dir, "rollback.sql");
3811
+ writeFileSync14(file, script);
3546
3812
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3547
3813
  if (result.code !== 0) throw new Error(`Rollback failed:
3548
3814
  ${result.output.trim()}`);
3549
3815
  } finally {
3550
- rmSync2(dir, { recursive: true, force: true });
3816
+ rmSync3(dir, { recursive: true, force: true });
3551
3817
  }
3552
3818
  log2(pc14.green(`
3553
3819
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
@@ -3555,8 +3821,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3555
3821
  }
3556
3822
 
3557
3823
  // 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";
3824
+ import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3825
+ import { dirname as dirname10, join as join20 } from "path";
3560
3826
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3561
3827
  import pc15 from "picocolors";
3562
3828
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
@@ -3575,10 +3841,10 @@ function handWrittenReason(source, header, options2 = {}) {
3575
3841
  return null;
3576
3842
  }
3577
3843
  function removeEmptyDirs(appRoot, start) {
3578
- let dir = dirname10(join19(appRoot, start));
3579
- const roots = [join19(appRoot, "app", "api"), join19(appRoot, "app", "dashboard")];
3844
+ let dir = dirname10(join20(appRoot, start));
3845
+ const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
3580
3846
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3581
- while (inside() && existsSync20(dir) && readdirSync7(dir).length === 0) {
3847
+ while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
3582
3848
  rmdirSync(dir);
3583
3849
  dir = dirname10(dir);
3584
3850
  }
@@ -3606,8 +3872,8 @@ async function rmResource(rawName, options2 = {}) {
3606
3872
  const owned = /* @__PURE__ */ new Set();
3607
3873
  if (entry) {
3608
3874
  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);
3875
+ for (const file of planFiles(all, void 0, readStack(appRoot))) {
3876
+ if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3611
3877
  }
3612
3878
  }
3613
3879
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3616,7 +3882,7 @@ async function rmResource(rawName, options2 = {}) {
3616
3882
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3617
3883
  const findings = [];
3618
3884
  for (const path of owned) {
3619
- const source = readFileSync12(join19(appRoot, path), "utf8");
3885
+ const source = readFileSync13(join20(appRoot, path), "utf8");
3620
3886
  let wrapper;
3621
3887
  if (entry && path === entry.file) {
3622
3888
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3637,15 +3903,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
3637
3903
  }
3638
3904
  const removed = [...owned].sort();
3639
3905
  for (const path of removed) {
3640
- unlinkSync2(join19(appRoot, path));
3906
+ unlinkSync2(join20(appRoot, path));
3641
3907
  removeEmptyDirs(appRoot, path);
3642
3908
  log2(`${pc15.red("remove".padEnd(9))} ${path}`);
3643
3909
  }
3644
- logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
3910
+ logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
3645
3911
  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"))
3912
+ if (table && existsSync21(join20(appRoot, "seeds"))) {
3913
+ const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
3914
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
3649
3915
  );
3650
3916
  if (seeds.length) log2(pc15.yellow(`
3651
3917
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
@@ -3715,9 +3981,9 @@ ${result.output.trim()}`);
3715
3981
  }
3716
3982
 
3717
3983
  // 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";
3984
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
3985
+ import { join as join22 } from "path";
3986
+ import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
3721
3987
  import { createJiti as createJiti4 } from "jiti";
3722
3988
  import pc17 from "picocolors";
3723
3989
 
@@ -3814,16 +4080,16 @@ function createInsertMany(sink, drizzle, onProgress) {
3814
4080
 
3815
4081
  // src/seed/local-d1.ts
3816
4082
  import { createRequire } from "module";
3817
- import { join as join20 } from "path";
4083
+ import { join as join21 } from "path";
3818
4084
  import { pathToFileURL } from "url";
3819
4085
  var LOCAL_STATE = ".wrangler/state/v3";
3820
4086
  async function openLocalD1(appRoot, binding) {
3821
4087
  process.env.WRANGLER_LOG ??= "error";
3822
- const appRequire = createRequire(join20(appRoot, "package.json"));
4088
+ const appRequire = createRequire(join21(appRoot, "package.json"));
3823
4089
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3824
4090
  const { env, dispose } = await wrangler.getPlatformProxy({
3825
- configPath: join20(appRoot, "wrangler.jsonc"),
3826
- persist: { path: join20(appRoot, LOCAL_STATE) }
4091
+ configPath: join21(appRoot, "wrangler.jsonc"),
4092
+ persist: { path: join21(appRoot, LOCAL_STATE) }
3827
4093
  });
3828
4094
  const bindings = env;
3829
4095
  const name = binding ?? "DB";
@@ -3838,8 +4104,8 @@ async function openLocalD1(appRoot, binding) {
3838
4104
  // src/commands/seed.ts
3839
4105
  var SEEDS_DIR = "seeds";
3840
4106
  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() : [];
4107
+ const dir = join22(appRoot, SEEDS_DIR);
4108
+ const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3843
4109
  if (!names?.length) return all;
3844
4110
  return names.map((name) => {
3845
4111
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3857,16 +4123,20 @@ async function runSeeds(options2 = {}) {
3857
4123
  log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
3858
4124
  return;
3859
4125
  }
3860
- const jiti = createJiti4(join21(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4126
+ if (readStack(appRoot) === "next") {
4127
+ await runPrismaSeeds(appRoot, files, log2);
4128
+ return;
4129
+ }
4130
+ const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3861
4131
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3862
4132
  const drizzleHelpers = await jiti.import("drizzle-orm");
3863
- const schema = await jiti.import(join21(appRoot, "db/schema.ts"));
4133
+ const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
3864
4134
  const local = await openLocalD1(appRoot);
3865
4135
  try {
3866
4136
  const db = drizzle(local.db, { schema });
3867
4137
  const insertMany = createInsertMany(local.db, drizzleHelpers);
3868
4138
  for (const file of files) {
3869
- const mod = await jiti.import(join21(appRoot, SEEDS_DIR, file));
4139
+ const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
3870
4140
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3871
4141
  log2(`${pc17.cyan("seed")} ${file}`);
3872
4142
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
@@ -3916,9 +4186,17 @@ function sampleValue(key, def) {
3916
4186
  return void 0;
3917
4187
  }
3918
4188
  }
3919
- function renderSeed(resource) {
4189
+ function renderSeed(resource, stack = "cloudflare") {
3920
4190
  if (!resource) {
3921
- return `import { defineSeed } from "@flaredev/core";
4191
+ return stack === "next" ? `import { defineSeed } from "@flaredev/core";
4192
+
4193
+ export default defineSeed(async ({ db, insertMany, fake, log }) => {
4194
+ // \`db\` is this app's Prisma client, e.g.:
4195
+ // await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
4196
+ // Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
4197
+ log("nothing to seed yet");
4198
+ });
4199
+ ` : `import { defineSeed } from "@flaredev/core";
3922
4200
 
3923
4201
  export default defineSeed(async ({ db, insertMany, fake, log }) => {
3924
4202
  // Insert rows with Drizzle, e.g.:
@@ -3931,11 +4209,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
3931
4209
  const table = tableExport(resource);
3932
4210
  const lines = [];
3933
4211
  const todos = [];
3934
- for (const [key, def] of storedFields2(resource)) {
4212
+ for (const [key, def] of storedFields3(resource)) {
3935
4213
  const value = sampleValue(key, def);
3936
4214
  if (value !== void 0) lines.push(` ${key}: ${value},`);
3937
4215
  else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
3938
4216
  }
4217
+ if (stack === "next") {
4218
+ const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
4219
+ return `import { defineSeed } from "@flaredev/core";
4220
+
4221
+ /** How many to create. createMany sends them in one statement. */
4222
+ const COUNT = 50;
4223
+
4224
+ export default defineSeed(async ({ insertMany, fake, log }) => {
4225
+ await insertMany(
4226
+ ${JSON.stringify(camelCase6(resource.name))},
4227
+ Array.from({ length: COUNT }, () => ({
4228
+ ${body}
4229
+ updatedAt: new Date(),
4230
+ })),
4231
+ );
4232
+ log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
4233
+ });
4234
+ `;
4235
+ }
3939
4236
  return `import { defineSeed } from "@flaredev/core";
3940
4237
  import { ${table} } from "@/db/schema";
3941
4238
 
@@ -3956,8 +4253,8 @@ async function makeSeed(rawName, options2 = {}) {
3956
4253
  const name = kebabCase4(rawName);
3957
4254
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3958
4255
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3959
- const path = join21(appRoot, relative2);
3960
- if (existsSync21(path)) throw new Error(`${relative2} already exists.`);
4256
+ const path = join22(appRoot, relative2);
4257
+ if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
3961
4258
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3962
4259
  let resource;
3963
4260
  if (options2.resource) {
@@ -3966,18 +4263,58 @@ async function makeSeed(rawName, options2 = {}) {
3966
4263
  } else {
3967
4264
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
3968
4265
  }
3969
- mkdirSync11(join21(appRoot, SEEDS_DIR), { recursive: true });
3970
- writeFileSync14(path, renderSeed(resource));
4266
+ mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4267
+ writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
3971
4268
  log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
3972
4269
  log2(`
3973
4270
  Run it with ${pc17.bold(`flare seed ${name}`)}.`);
3974
4271
  return relative2;
3975
4272
  }
4273
+ async function runPrismaSeeds(appRoot, files, log2) {
4274
+ const runner = join22(appRoot, ".flare-seed-runner.mts");
4275
+ const script = [
4276
+ `import "dotenv/config";`,
4277
+ `import { createFake } from "@flaredev/core";`,
4278
+ `import { prisma } from "./lib/db";`,
4279
+ ...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
4280
+ "",
4281
+ `const files = ${JSON.stringify(files)};`,
4282
+ `const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
4283
+ `const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
4284
+ "",
4285
+ `for (const [index, seed] of seeds.entries()) {`,
4286
+ ` console.log("seed " + files[index]);`,
4287
+ ` await seed({`,
4288
+ ` db: prisma,`,
4289
+ ` env: process.env,`,
4290
+ ` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
4291
+ ` const delegate = client[model];`,
4292
+ ` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
4293
+ ` await delegate.createMany({ data: rows });`,
4294
+ ` },`,
4295
+ ` fake: createFake(),`,
4296
+ ` log: (message: string) => console.log(" " + message),`,
4297
+ ` });`,
4298
+ `}`,
4299
+ `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4300
+ ""
4301
+ ].join("\n");
4302
+ writeFileSync15(runner, script);
4303
+ try {
4304
+ const tsx = resolveBin(appRoot, "tsx", "tsx");
4305
+ const code = await runNode([tsx, runner], appRoot);
4306
+ if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4307
+ log2(pc17.green(`
4308
+ Ran ${files.length} seed(s).`));
4309
+ } finally {
4310
+ rmSync4(runner, { force: true });
4311
+ }
4312
+ }
3976
4313
 
3977
4314
  // src/commands/db-push.ts
3978
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4315
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
3979
4316
  import { tmpdir as tmpdir2 } from "os";
3980
- import { join as join22 } from "path";
4317
+ import { join as join23 } from "path";
3981
4318
  import pc18 from "picocolors";
3982
4319
 
3983
4320
  // src/terminal.ts
@@ -4042,7 +4379,7 @@ async function dbPush(tables, options2 = {}) {
4042
4379
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4043
4380
  const started = Date.now();
4044
4381
  const local = await openLocalD1(appRoot, options2.database);
4045
- const dir = mkdtempSync2(join22(tmpdir2(), "flare-push-"));
4382
+ const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4046
4383
  try {
4047
4384
  const statements = [];
4048
4385
  const counts = [];
@@ -4068,8 +4405,8 @@ async function dbPush(tables, options2 = {}) {
4068
4405
  log2("Nothing to push: those tables are empty locally.");
4069
4406
  return { tables: counts, ms: Date.now() - started };
4070
4407
  }
4071
- const file = join22(dir, "push.sql");
4072
- writeFileSync15(file, statements.join("\n"));
4408
+ const file = join23(dir, "push.sql");
4409
+ writeFileSync16(file, statements.join("\n"));
4073
4410
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4074
4411
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4075
4412
  if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
@@ -4078,19 +4415,19 @@ ${result.output.trim()}`);
4078
4415
  log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4079
4416
  return { tables: counts, ms };
4080
4417
  } finally {
4081
- rmSync3(dir, { recursive: true, force: true });
4418
+ rmSync5(dir, { recursive: true, force: true });
4082
4419
  await local.dispose();
4083
4420
  }
4084
4421
  }
4085
4422
 
4086
4423
  // src/commands/seed-resource.ts
4087
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4424
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
4088
4425
  import { tmpdir as tmpdir3 } from "os";
4089
- import { join as join23 } from "path";
4426
+ import { join as join24 } from "path";
4090
4427
  import pc19 from "picocolors";
4091
4428
 
4092
4429
  // src/seed/rows.ts
4093
- import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
4430
+ import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
4094
4431
  var has = (key, ...words) => words.some((word) => key.includes(word));
4095
4432
  function stringValue(key, def, fake, resource, index) {
4096
4433
  switch (def.format) {
@@ -4195,12 +4532,12 @@ function digitalish(resource) {
4195
4532
  );
4196
4533
  }
4197
4534
  function seedColumns(resource) {
4198
- return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4535
+ return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
4199
4536
  }
4200
4537
  function* seedRows(resource, count, options2 = {}) {
4201
4538
  const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
4202
4539
  const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
4203
- const fields = storedFields3(resource);
4540
+ const fields = storedFields4(resource);
4204
4541
  const now = options2.now ?? Date.now();
4205
4542
  const start = options2.startIndex ?? 0;
4206
4543
  for (let i = 0; i < count; i++) {
@@ -4209,7 +4546,7 @@ function* seedRows(resource, count, options2 = {}) {
4209
4546
  for (const [key, def] of fields) {
4210
4547
  let value = fieldValue(key, def, fake, resource, parentIds, number);
4211
4548
  if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
4212
- row[columnName2(key)] = value;
4549
+ row[columnName3(key)] = value;
4213
4550
  }
4214
4551
  const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
4215
4552
  row.created_at = created;
@@ -4288,6 +4625,11 @@ async function seedResourceCommand(name, options2 = {}) {
4288
4625
  async function seedResource(name, options2 = {}) {
4289
4626
  const log2 = options2.log ?? ((message) => console.log(message));
4290
4627
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4628
+ if (readStack(appRoot) === "next") {
4629
+ throw new Error(
4630
+ "flare seed:resource is Cloudflare-only so far. On this stack, write a seed with `flare seed:make <name> --resource " + name + "` \u2014 it comes with rows already filled in \u2014 and run it with `flare seed`."
4631
+ );
4632
+ }
4291
4633
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4292
4634
  const resource = pickResource(resources, name);
4293
4635
  const total = parseCount(options2.count);
@@ -4320,8 +4662,8 @@ ${result.output.trim()}`);
4320
4662
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4321
4663
  startIndex = counted?.total ?? 0;
4322
4664
  }
4323
- const dir = mkdtempSync3(join23(tmpdir3(), "flare-seed-"));
4324
- const file = join23(dir, `${resource.table}.sql`);
4665
+ const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4666
+ const file = join24(dir, `${resource.table}.sql`);
4325
4667
  try {
4326
4668
  const statements = [];
4327
4669
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4331,13 +4673,13 @@ ${result.output.trim()}`);
4331
4673
  written += statement.rows;
4332
4674
  options2.onProgress?.(written, total);
4333
4675
  }
4334
- writeFileSync16(file, statements.join("\n"));
4676
+ writeFileSync17(file, statements.join("\n"));
4335
4677
  log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4336
4678
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4337
4679
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4338
4680
  ${result.output.trim()}`);
4339
4681
  } finally {
4340
- rmSync4(dir, { recursive: true, force: true });
4682
+ rmSync6(dir, { recursive: true, force: true });
4341
4683
  }
4342
4684
  return { resource, rows: total, ms: Date.now() - started, where };
4343
4685
  }
@@ -4364,17 +4706,17 @@ ${result.output.trim()}`);
4364
4706
 
4365
4707
  // src/commands/billing-sync.ts
4366
4708
  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";
4709
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
4368
4710
  import { tmpdir as tmpdir4 } from "os";
4369
- import { join as join24 } from "path";
4711
+ import { join as join25 } from "path";
4370
4712
  import { pathToFileURL as pathToFileURL2 } from "url";
4371
4713
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4372
4714
  import pc20 from "picocolors";
4373
4715
  function stripeKeys(appRoot) {
4374
4716
  const map = {};
4375
- const path = join24(appRoot, ".dev.vars");
4376
- if (existsSync22(path)) {
4377
- for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
4717
+ const path = join25(appRoot, ".dev.vars");
4718
+ if (existsSync23(path)) {
4719
+ for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
4378
4720
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4379
4721
  if (match) map[match[1]] = match[2].trim();
4380
4722
  }
@@ -4457,10 +4799,10 @@ async function syncPlans(options2 = {}) {
4457
4799
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4458
4800
  const db = readD1Databases(appRoot)[0];
4459
4801
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4460
- const appRequire = createRequire2(join24(appRoot, "package.json"));
4802
+ const appRequire = createRequire2(join25(appRoot, "package.json"));
4461
4803
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4462
4804
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4463
- const appName = JSON.parse(readFileSync13(join24(appRoot, "package.json"), "utf8")).name ?? "app";
4805
+ const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
4464
4806
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4465
4807
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4466
4808
  if (products.length === 0) {
@@ -4481,15 +4823,15 @@ async function syncPlans(options2 = {}) {
4481
4823
  ${existingResult.output.trim()}`);
4482
4824
  }
4483
4825
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4484
- const dir = mkdtempSync4(join24(tmpdir4(), "flare-sync-"));
4826
+ const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
4485
4827
  try {
4486
- const file = join24(dir, "sync-plans.sql");
4487
- writeFileSync17(file, renderSyncSql(rows));
4828
+ const file = join25(dir, "sync-plans.sql");
4829
+ writeFileSync18(file, renderSyncSql(rows));
4488
4830
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4489
4831
  if (result.code !== 0) throw new Error(`Could not write plans:
4490
4832
  ${result.output.trim()}`);
4491
4833
  } finally {
4492
- rmSync5(dir, { recursive: true, force: true });
4834
+ rmSync7(dir, { recursive: true, force: true });
4493
4835
  }
4494
4836
  let created = 0;
4495
4837
  for (const row of rows) {
@@ -4507,16 +4849,16 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
4507
4849
  }
4508
4850
 
4509
4851
  // src/commands/sync.ts
4510
- import { existsSync as existsSync23 } from "fs";
4511
- import { join as join25 } from "path";
4852
+ import { existsSync as existsSync24 } from "fs";
4853
+ import { join as join26 } from "path";
4512
4854
  import pc21 from "picocolors";
4513
4855
  async function syncTypes(options2 = {}) {
4514
4856
  const log2 = options2.log ?? ((message) => console.log(message));
4515
4857
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4516
4858
  const all = await loadResources(appRoot);
4517
4859
  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);
4860
+ const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
4861
+ const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4520
4862
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4521
4863
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
4522
4864
  logResults(results, log2);
@@ -4600,22 +4942,22 @@ ${result.output.trim()}`);
4600
4942
 
4601
4943
  // src/utils/install.ts
4602
4944
  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";
4945
+ import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
4946
+ import { join as join27 } from "path";
4605
4947
  function expectedPackages(dir) {
4606
- const npmLock = join26(dir, "package-lock.json");
4607
- if (existsSync24(npmLock)) {
4948
+ const npmLock = join27(dir, "package-lock.json");
4949
+ if (existsSync25(npmLock)) {
4608
4950
  try {
4609
- const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
4951
+ const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
4610
4952
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4611
4953
  } catch {
4612
4954
  return 0;
4613
4955
  }
4614
4956
  }
4615
- const pnpmLock = join26(dir, "pnpm-lock.yaml");
4616
- if (existsSync24(pnpmLock)) {
4957
+ const pnpmLock = join27(dir, "pnpm-lock.yaml");
4958
+ if (existsSync25(pnpmLock)) {
4617
4959
  try {
4618
- const text = readFileSync14(pnpmLock, "utf8");
4960
+ const text = readFileSync15(pnpmLock, "utf8");
4619
4961
  const packages = text.slice(text.indexOf("\npackages:"));
4620
4962
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4621
4963
  } catch {
@@ -4625,15 +4967,15 @@ function expectedPackages(dir) {
4625
4967
  return 0;
4626
4968
  }
4627
4969
  function installedPackages(dir) {
4628
- const root = join26(dir, "node_modules");
4629
- if (!existsSync24(root)) return 0;
4970
+ const root = join27(dir, "node_modules");
4971
+ if (!existsSync25(root)) return 0;
4630
4972
  let total = 0;
4631
4973
  try {
4632
4974
  for (const entry of readdirSync9(root, { withFileTypes: true })) {
4633
4975
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4634
4976
  if (entry.name.startsWith("@")) {
4635
4977
  try {
4636
- total += readdirSync9(join26(root, entry.name)).length;
4978
+ total += readdirSync9(join27(root, entry.name)).length;
4637
4979
  } catch {
4638
4980
  }
4639
4981
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -4677,31 +5019,33 @@ function install(packageManager, dir, onProgress) {
4677
5019
  // src/cli.ts
4678
5020
  function createCli() {
4679
5021
  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;
5022
+ cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
5023
+ async (dir, options2) => {
5024
+ const wanted = !options2.skipInstall;
5025
+ let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
5026
+ if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
5027
+ const result = createApp(dir, { pm: options2.pm, stack: options2.stack, install: false, ...answers, log: (message) => prompts3.log.message(message) });
5028
+ let installed = false;
5029
+ if (wanted) {
5030
+ const spin = prompts3.spinner({ indicator: "timer" });
5031
+ spin.start(`Installing dependencies with ${result.packageManager}`);
5032
+ const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
5033
+ spin.message(
5034
+ percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
5035
+ );
5036
+ });
5037
+ installed = outcome.code === 0;
5038
+ if (installed) {
5039
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
5040
+ } else {
5041
+ spin.error(`${result.packageManager} install failed`);
5042
+ process.stderr.write(outcome.output);
5043
+ process.exitCode = 1;
5044
+ }
4701
5045
  }
5046
+ printNextSteps(result, installed);
4702
5047
  }
4703
- printNextSteps(result, installed);
4704
- });
5048
+ );
4705
5049
  cli.command("gen <generator> [name] [second]", "Generate code. Generators: resource, endpoint, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
4706
5050
  async (generator, name, second, options2) => {
4707
5051
  if (generator === "billing")