@flaredev/cli 0.9.1 → 0.11.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.
package/dist/index.js CHANGED
@@ -141,12 +141,12 @@ function parseTheme(value) {
141
141
  // src/versions.ts
142
142
  var COMPATIBILITY_DATE = "2026-09-15";
143
143
  var APP_DEPENDENCIES = {
144
- "@better-auth/drizzle-adapter": "1.7.7",
145
- "@better-auth/passkey": "1.7.7",
144
+ "@better-auth/drizzle-adapter": "1.7.14",
145
+ "@better-auth/passkey": "1.7.14",
146
146
  // Server components import Slot from here rather than the "radix-ui" umbrella (see components/ui/button.tsx).
147
147
  "@radix-ui/react-slot": "1.3.3",
148
148
  "@vinext/cloudflare": "1.0.0-beta.8",
149
- "better-auth": "1.7.7",
149
+ "better-auth": "1.7.14",
150
150
  // Admin UI (shadcn/ui primitives in components/ui).
151
151
  "class-variance-authority": "^0.7.1",
152
152
  clsx: "^2.1.1",
@@ -172,7 +172,7 @@ var APP_DEPENDENCIES = {
172
172
  "react-server-dom-webpack": "19.3.0",
173
173
  vinext: "1.0.0-beta.10"
174
174
  };
175
- var BETTER_AUTH_VERSION = "1.7.7";
175
+ var BETTER_AUTH_VERSION = "1.7.14";
176
176
  var API_KEY_PACKAGE = "@better-auth/api-key";
177
177
  var APP_DEV_DEPENDENCIES = {
178
178
  "@cloudflare/vite-plugin": "^1.54.10",
@@ -189,12 +189,16 @@ var APP_DEV_DEPENDENCIES = {
189
189
  wrangler: "^4.132.0"
190
190
  };
191
191
  var NEXT_DEPENDENCIES = {
192
- next: "16.0.4",
192
+ // 16.0.4 was carrying 36 advisories, three of them critical: unauthenticated RCE in the
193
+ // image optimizer and on Windows hosts (both fixed in 16.3.3), and RCE in the flight
194
+ // protocol. Vercel refuses to build it. A pin is only worth having if it is moved when
195
+ // one of these lands, so treat a critical against this line as a release of its own.
196
+ next: "16.3.8",
193
197
  "@prisma/client": "7.10.0",
194
198
  "@prisma/adapter-pg": "7.10.0",
195
199
  pg: "^8.16.3",
196
200
  "@upstash/redis": "^1.36.0",
197
- "@better-auth/prisma-adapter": "1.7.7",
201
+ "@better-auth/prisma-adapter": "1.7.14",
198
202
  // Signs S3 requests in a few kilobytes, where the AWS SDK is megabytes.
199
203
  aws4fetch: "^1.0.20",
200
204
  dotenv: "^17.2.3"
@@ -229,7 +233,7 @@ var CLOUDFLARE_ONLY = [
229
233
  // src/stack.ts
230
234
  import { existsSync, readFileSync, writeFileSync } from "fs";
231
235
  import { join } from "path";
232
- var STACKS = ["cloudflare", "next"];
236
+ var STACKS = ["next", "cloudflare"];
233
237
  var isStack = (value) => STACKS.includes(value);
234
238
  var STACK_LABELS = {
235
239
  cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
@@ -353,7 +357,7 @@ function createApp(target, options2 = {}) {
353
357
  if (options2.stack !== void 0 && !isStack(options2.stack)) {
354
358
  throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
355
359
  }
356
- const stack = options2.stack ?? "cloudflare";
360
+ const stack = options2.stack ?? "next";
357
361
  const authProviders = parseAuthProviders(options2.authProviders);
358
362
  const authMethods = parseAuthMethods(options2.auth);
359
363
  const theme = parseTheme(options2.theme);
@@ -372,9 +376,8 @@ function createApp(target, options2 = {}) {
372
376
  writeFileSync3(join4(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
373
377
  appendFileSync(join4(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
374
378
  const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
375
- const uiOnly = Object.fromEntries(
376
- Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
377
- );
379
+ const cloudflareOnlyPackages = ["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack", "@better-auth/drizzle-adapter"];
380
+ const uiOnly = Object.fromEntries(Object.entries(APP_DEPENDENCIES).filter(([key]) => !cloudflareOnlyPackages.includes(key)));
378
381
  writeJson(join4(dir, "package.json"), {
379
382
  name,
380
383
  version: "0.1.0",
@@ -413,7 +416,8 @@ function createApp(target, options2 = {}) {
413
416
  `BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
414
417
  RESEND_API_KEY=
415
418
  MAIL_FROM=
416
- ${devVarsEntries(authProviders)}`
419
+ ${devVarsEntries(authProviders)}` + (stack === "next" ? `
420
+ ${LOCAL_SERVICES}` : "")
417
421
  );
418
422
  if (packageManager === "pnpm" && !inWorkspace) {
419
423
  writeFileSync3(join4(dir, "pnpm-workspace.yaml"), `allowBuilds:
@@ -421,16 +425,28 @@ ${ALLOW_BUILDS}
421
425
  `);
422
426
  }
423
427
  const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
424
- if (pinned) copyLockfile(packageManager, dir);
428
+ if (pinned) copyLockfile(packageManager, dir, stack);
425
429
  const log2 = options2.log ?? ((message) => console.log(message));
426
430
  log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
427
431
  if (prefersPnpm) log2(pc.dim(`Installing with pnpm \u2014 far quicker for an app this size. Use ${pc.bold("--pm npm")} to change that.`));
428
432
  return { dir, name, packageManager, inWorkspace };
429
433
  }
430
- function copyLockfile(packageManager, dir) {
434
+ var LOCAL_SERVICES = [
435
+ "# Local services (docker-compose.yml). Swap these for Neon, R2 and Upstash to deploy.",
436
+ "DATABASE_URL=postgresql://flare:flare@127.0.0.1:5432/app",
437
+ "BETTER_AUTH_URL=http://localhost:3000",
438
+ "R2_ENDPOINT=http://127.0.0.1:9000",
439
+ "R2_BUCKET=app-uploads",
440
+ "R2_ACCESS_KEY_ID=flare",
441
+ "R2_SECRET_ACCESS_KEY=flare-secret-123",
442
+ "UPSTASH_REDIS_REST_URL=http://127.0.0.1:8079",
443
+ "UPSTASH_REDIS_REST_TOKEN=flare-local-token",
444
+ ""
445
+ ].join("\n");
446
+ function copyLockfile(packageManager, dir, stack) {
431
447
  const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
432
448
  if (!file) return false;
433
- const source = join4(templatesDir, "locks", file);
449
+ const source = join4(templatesDir, "locks", stack, file);
434
450
  if (!existsSync4(source)) return false;
435
451
  copyFileSync(source, join4(dir, file));
436
452
  return true;
@@ -1275,6 +1291,20 @@ function runNode(argv, cwd) {
1275
1291
  child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
1276
1292
  });
1277
1293
  }
1294
+ function runNodeCapturing(argv, cwd) {
1295
+ return new Promise((resolvePromise, reject) => {
1296
+ const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"] });
1297
+ let output = "";
1298
+ for (const stream of [child.stdout, child.stderr]) {
1299
+ stream?.on("data", (chunk) => {
1300
+ output += chunk.toString();
1301
+ process.stdout.write(chunk);
1302
+ });
1303
+ }
1304
+ child.on("error", reject);
1305
+ child.on("exit", (code, signal) => resolvePromise({ code: code ?? (signal ? 1 : 0), output }));
1306
+ });
1307
+ }
1278
1308
  function runNodeWithTunnel(argv, cwd, opened) {
1279
1309
  return new Promise((resolvePromise, reject) => {
1280
1310
  const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
@@ -1674,11 +1704,37 @@ function list(raw, what, spec) {
1674
1704
  return items;
1675
1705
  }
1676
1706
  var RESOURCE_NAME = /^[A-Z][A-Za-z0-9]*$/;
1707
+ var CONDITION = /^(.*?)\s+if\s+(.+)$/s;
1708
+ var COMPARISON = /^([A-Za-z][A-Za-z0-9_]*)\s*(==|!=|\bin\b)\s*(.+)$/;
1709
+ function parseCondition(clause, spec) {
1710
+ const match = COMPARISON.exec(clause.trim());
1711
+ if (!match) {
1712
+ throw new FieldGrammarError(`"${spec}": "${clause.trim()}" isn't a condition. Write it like "if kind == recurring".`);
1713
+ }
1714
+ const [, dependsOn, operator, rawValue] = match;
1715
+ const op = operator === "==" ? "eq" : operator === "!=" ? "ne" : "in";
1716
+ const text = rawValue.trim();
1717
+ if (op === "in") {
1718
+ const inner = /^\((.*)\)$/.exec(text);
1719
+ if (!inner) throw new FieldGrammarError(`"${spec}": "in" takes a list in brackets, e.g. "if kind in (recurring, retainer)".`);
1720
+ return { field: dependsOn, op, value: list(inner[1], "condition value", spec) };
1721
+ }
1722
+ if (!text || /\s/.test(text)) {
1723
+ throw new FieldGrammarError(`"${spec}": "${text}" isn't a single value. Write it like "if kind == recurring".`);
1724
+ }
1725
+ return { field: dependsOn, op, value: text };
1726
+ }
1677
1727
  function parseField(spec) {
1678
1728
  const colon = spec.indexOf(":");
1679
1729
  if (colon <= 0) throw new FieldGrammarError(`"${spec}": expected name:type, e.g. "title:string".`);
1680
1730
  const rawName = spec.slice(0, colon).trim();
1681
1731
  let type = spec.slice(colon + 1).trim();
1732
+ let showWhen;
1733
+ const conditioned = CONDITION.exec(type);
1734
+ if (conditioned) {
1735
+ type = conditioned[1].trim();
1736
+ showWhen = parseCondition(conditioned[2], spec);
1737
+ }
1682
1738
  let required = true;
1683
1739
  let unique = false;
1684
1740
  while (/[?!]$/.test(type)) {
@@ -1691,6 +1747,7 @@ function parseField(spec) {
1691
1747
  }
1692
1748
  let key = camelCase(rawName);
1693
1749
  const field = { key, kind: "string", required, unique };
1750
+ if (showWhen) field.showWhen = showWhen;
1694
1751
  let match;
1695
1752
  if (SIMPLE_KINDS.includes(type)) {
1696
1753
  field.kind = type;
@@ -1768,6 +1825,40 @@ function parseFields(input) {
1768
1825
  if (seen.has(field.key)) throw new FieldGrammarError(`Field "${field.key}" is declared more than once.`);
1769
1826
  seen.add(field.key);
1770
1827
  }
1828
+ const byKey = new Map(fields.map((field) => [field.key, field]));
1829
+ for (const field of fields) {
1830
+ const condition = field.showWhen;
1831
+ if (!condition) continue;
1832
+ if (condition.field === field.key) {
1833
+ throw new FieldGrammarError(`Field "${field.key}" cannot depend on itself.`);
1834
+ }
1835
+ const target = byKey.get(condition.field);
1836
+ if (!target) {
1837
+ const names = fields.map((other) => other.key).filter((name) => name !== field.key);
1838
+ throw new FieldGrammarError(
1839
+ `Field "${field.key}" depends on "${condition.field}", which isn't a field here.${suggest(condition.field, names)} Fields: ${names.join(", ")}.`
1840
+ );
1841
+ }
1842
+ if (target.showWhen) {
1843
+ throw new FieldGrammarError(`Field "${field.key}" depends on "${target.key}", which is itself conditional. Conditions only go one level deep.`);
1844
+ }
1845
+ const values = Array.isArray(condition.value) ? condition.value : [condition.value];
1846
+ if (target.options) {
1847
+ for (const value of values) {
1848
+ if (!target.options.includes(value)) {
1849
+ throw new FieldGrammarError(
1850
+ `Field "${field.key}" is shown when ${condition.field} is "${value}", which isn't one of its options.${suggest(value, target.options)} Options: ${target.options.join(", ")}.`
1851
+ );
1852
+ }
1853
+ }
1854
+ } else if (target.kind === "boolean") {
1855
+ for (const value of values) {
1856
+ if (value !== "true" && value !== "false") {
1857
+ throw new FieldGrammarError(`Field "${field.key}" depends on the boolean "${condition.field}", so compare it with true or false, not "${value}".`);
1858
+ }
1859
+ }
1860
+ }
1861
+ }
1771
1862
  return fields;
1772
1863
  }
1773
1864
 
@@ -1789,6 +1880,7 @@ function options(field) {
1789
1880
  if (field.widget) opts.widget = field.widget;
1790
1881
  if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
1791
1882
  if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
1883
+ if (field.showWhen) opts.showWhen = field.showWhen;
1792
1884
  return opts;
1793
1885
  }
1794
1886
  function toField(field) {
@@ -1812,7 +1904,13 @@ function toField(field) {
1812
1904
  return builders[field.kind](opts);
1813
1905
  }
1814
1906
  }
1815
- var literal = (value) => JSON.stringify(value);
1907
+ var literal = (value) => {
1908
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1909
+ const entries = Object.entries(value);
1910
+ return `{ ${entries.map(([key, inner]) => `${/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key)}: ${literal(inner)}`).join(", ")} }`;
1911
+ }
1912
+ return JSON.stringify(value);
1913
+ };
1816
1914
  function renderOptions(opts) {
1817
1915
  const entries = Object.entries(opts);
1818
1916
  return entries.length ? `{ ${entries.map(([k, v]) => `${k}: ${literal(v)}`).join(", ")} }` : "";
@@ -2644,7 +2742,7 @@ function prismaType(resource, key, def) {
2644
2742
  case "int":
2645
2743
  return "Int";
2646
2744
  case "float":
2647
- return "Float";
2745
+ return def.format === "money" ? "BigInt" : "Float";
2648
2746
  case "boolean":
2649
2747
  return "Boolean";
2650
2748
  case "date":
@@ -2713,7 +2811,7 @@ function renderPrismaModel(entry, all) {
2713
2811
  if (def.kind === "belongsTo") {
2714
2812
  const target = byName.get(def.target);
2715
2813
  if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
2716
- const optional2 = def.required ? "" : "?";
2814
+ const optional2 = def.required && !def.showWhen ? "" : "?";
2717
2815
  const relationField = key.replace(/Id$/, "");
2718
2816
  const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
2719
2817
  const relationArgs = [
@@ -2727,7 +2825,7 @@ function renderPrismaModel(entry, all) {
2727
2825
  indexes.push(` @@index([${key}])`);
2728
2826
  continue;
2729
2827
  }
2730
- const optional = def.required || def.kind === "multiselect" || def.kind === "tags" ? "" : "?";
2828
+ const optional = def.required && !def.showWhen || def.kind === "multiselect" || def.kind === "tags" ? "" : "?";
2731
2829
  lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
2732
2830
  }
2733
2831
  for (const back of backReferences(resource, all)) {
@@ -2773,7 +2871,7 @@ function columnExpression(key, def, resources, selfName) {
2773
2871
  expr = `integer(${col})`;
2774
2872
  break;
2775
2873
  case "float":
2776
- expr = `real(${col})`;
2874
+ expr = def.format === "money" ? `integer(${col})` : `real(${col})`;
2777
2875
  break;
2778
2876
  case "boolean":
2779
2877
  expr = `integer(${col}, { mode: "boolean" })`;
@@ -2802,7 +2900,7 @@ function columnExpression(key, def, resources, selfName) {
2802
2900
  default:
2803
2901
  expr = `text(${col})`;
2804
2902
  }
2805
- if (def.required) expr += ".notNull()";
2903
+ if (def.required && !def.showWhen) expr += ".notNull()";
2806
2904
  if ("unique" in def && def.unique) expr += ".unique()";
2807
2905
  if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
2808
2906
  return expr;
@@ -4119,7 +4217,7 @@ async function askCreateQuestions(given, appName) {
4119
4217
  answers.stack = stopIfCancelled(
4120
4218
  await prompts2.select({
4121
4219
  message: "Where will it run?",
4122
- initialValue: "cloudflare",
4220
+ initialValue: "next",
4123
4221
  options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
4124
4222
  })
4125
4223
  );
@@ -4213,13 +4311,22 @@ async function genResource(rawName, options2) {
4213
4311
  }
4214
4312
  }
4215
4313
  }
4314
+ const policyRelative = policyPath(name);
4315
+ const policyAbsolute = join17(appRoot, policyRelative);
4316
+ const wrotePolicy = !exists && !existsSync18(policyAbsolute);
4317
+ if (wrotePolicy) {
4318
+ mkdirSync8(dirname9(policyAbsolute), { recursive: true });
4319
+ writeFileSync11(policyAbsolute, renderPolicy(name, { read: ["admin"], create: ["admin"], update: ["admin"], delete: ["admin"] }));
4320
+ log2(`${pc11.green("create".padEnd(9))} ${policyRelative}`);
4321
+ }
4216
4322
  let written;
4217
4323
  let table;
4218
4324
  try {
4219
4325
  const all = await loadResources(appRoot);
4220
4326
  table = all.find(({ resource }) => resource.name === name).resource.table;
4221
4327
  ensureSupportFiles(appRoot, log2);
4222
- written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
4328
+ const policies = wrotePolicy ? await loadPolicies(appRoot) : void 0;
4329
+ written = applyPlan(appRoot, planFiles(all, policies, readStack(appRoot)), { force: options2.force });
4223
4330
  logResults(written, log2);
4224
4331
  } catch (error) {
4225
4332
  const hint = exists ? "" : `
@@ -4247,7 +4354,8 @@ Next: ${pc11.bold("flare migrate")} to apply it locally.`);
4247
4354
  }
4248
4355
  }
4249
4356
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
4250
- return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
4357
+ const policy = wrotePolicy ? [{ path: policyRelative, status: "missing", outcome: "create" }] : [];
4358
+ return { name, mode: exists ? "update" : "create", written: [descriptor, ...policy, ...written], migrations };
4251
4359
  }
4252
4360
 
4253
4361
  // src/commands/gen-endpoint.ts
@@ -4314,10 +4422,88 @@ ${body}
4314
4422
  }
4315
4423
 
4316
4424
  // src/commands/gen-migration.ts
4317
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
4425
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, writeFileSync as writeFileSync13 } from "fs";
4318
4426
  import { basename as basename2, join as join19 } from "path";
4319
4427
  import { snakeCase } from "@flaredev/core";
4320
4428
  import pc13 from "picocolors";
4429
+
4430
+ // src/generator/destructive.ts
4431
+ function stripNoise(sql2) {
4432
+ let out = "";
4433
+ let index = 0;
4434
+ while (index < sql2.length) {
4435
+ const two = sql2.slice(index, index + 2);
4436
+ if (two === "--") {
4437
+ const end = sql2.indexOf("\n", index);
4438
+ const stop = end === -1 ? sql2.length : end;
4439
+ out += " ".repeat(stop - index);
4440
+ index = stop;
4441
+ continue;
4442
+ }
4443
+ if (two === "/*") {
4444
+ const end = sql2.indexOf("*/", index + 2);
4445
+ const skipped = sql2.slice(index, end === -1 ? sql2.length : end + 2);
4446
+ out += skipped.replaceAll(/[^\n]/g, " ");
4447
+ index = end === -1 ? sql2.length : end + 2;
4448
+ continue;
4449
+ }
4450
+ const char = sql2[index];
4451
+ if (char === "'" || char === '"' || char === "`") {
4452
+ let at = index + 1;
4453
+ while (at < sql2.length) {
4454
+ if (sql2[at] === char) {
4455
+ if (sql2[at + 1] === char) at += 2;
4456
+ else break;
4457
+ } else at += 1;
4458
+ }
4459
+ const literal2 = sql2.slice(index, Math.min(at + 1, sql2.length));
4460
+ out += literal2.replaceAll(/[^\n]/g, " ");
4461
+ index = at + 1;
4462
+ continue;
4463
+ }
4464
+ out += char;
4465
+ index += 1;
4466
+ }
4467
+ return out;
4468
+ }
4469
+ var PATTERNS = [
4470
+ [/\bDROP\s+COLUMN\b/i, "a column, and the values in it"],
4471
+ [/\bDROP\s+TABLE\b/i, "a table, and every row in it"],
4472
+ [/\bTRUNCATE\b/i, "every row in a table"],
4473
+ [/\bDROP\s+(?:MATERIALIZED\s+)?VIEW\b/i, "a view"],
4474
+ [/\bDROP\s+INDEX\b/i, "an index \u2014 if it was written by hand, Prisma cannot recreate it"],
4475
+ [/\bDROP\s+TRIGGER\b/i, "a trigger \u2014 Prisma does not know about it and will not put it back"],
4476
+ [/\bDROP\s+FUNCTION\b/i, "a function"],
4477
+ [/\bDROP\s+CONSTRAINT\b/i, "a constraint"],
4478
+ [/\bDROP\s+TYPE\b/i, "a type"],
4479
+ [/\bDROP\s+SEQUENCE\b/i, "a sequence"],
4480
+ [/\bDROP\s+SCHEMA\b/i, "a schema, and everything in it"],
4481
+ [/\bDROP\s+DATABASE\b/i, "the database"]
4482
+ ];
4483
+ function destructiveStatements(sql2) {
4484
+ const cleaned = stripNoise(sql2);
4485
+ const found = [];
4486
+ let start = 0;
4487
+ for (let index = 0; index <= cleaned.length; index += 1) {
4488
+ if (index !== cleaned.length && cleaned[index] !== ";") continue;
4489
+ const statement = cleaned.slice(start, index);
4490
+ const trimmed = statement.trim();
4491
+ const from = start;
4492
+ start = index + 1;
4493
+ if (!trimmed) continue;
4494
+ const match = PATTERNS.find(([pattern]) => pattern.test(trimmed));
4495
+ if (!match) continue;
4496
+ const offset = from + statement.search(/\S/);
4497
+ found.push({
4498
+ statement: sql2.slice(offset, index).trim().replaceAll(/\s+/g, " ").slice(0, 160),
4499
+ kind: match[1],
4500
+ line: cleaned.slice(0, offset).split("\n").length
4501
+ });
4502
+ }
4503
+ return found;
4504
+ }
4505
+
4506
+ // src/commands/gen-migration.ts
4321
4507
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
4322
4508
  -- Write SQL that undoes the up migration, e.g.:
4323
4509
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
@@ -4328,6 +4514,7 @@ async function genMigration(rawName, options2 = {}) {
4328
4514
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4329
4515
  const name = snakeCase(rawName);
4330
4516
  if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
4517
+ if (readStack(appRoot) === "next") return genPrismaMigration(appRoot, name, options2);
4331
4518
  const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
4332
4519
  if (files.length === 0) {
4333
4520
  log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
@@ -4352,6 +4539,89 @@ Next: ${pc13.bold("flare migrate")}.`);
4352
4539
  }
4353
4540
  return files;
4354
4541
  }
4542
+ async function genPrismaMigration(appRoot, name, options2) {
4543
+ const log2 = options2.log ?? ((message) => console.log(message));
4544
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
4545
+ const migrations = join19(appRoot, "prisma", "migrations");
4546
+ const before = new Set(existsSync20(migrations) ? readdirSync7(migrations) : []);
4547
+ log2(`${pc13.cyan("prisma")} migrate dev --create-only --name ${name}`);
4548
+ const { code, output } = await runNodeCapturing([prisma, "migrate", "dev", "--create-only", "--name", name], appRoot);
4549
+ if (code !== 0) {
4550
+ if (/drift detected|we need to reset/i.test(output)) {
4551
+ log2("");
4552
+ log2(pc13.yellow("Prisma found something in the database that its schema does not describe."));
4553
+ log2(
4554
+ [
4555
+ "",
4556
+ `Nothing has been changed \u2014 ${pc13.bold("--create-only")} writes a migration, it does not apply one, and no`,
4557
+ "migration was written. Do not run `prisma migrate reset`: it drops the database, and what it",
4558
+ "would drop is the thing Prisma is complaining it cannot see.",
4559
+ "",
4560
+ "Drift above is reported as changes to get from your migrations to the database, so it lists",
4561
+ "what the database has and the migrations do not. Usually that is something added by hand:",
4562
+ "",
4563
+ ` ${pc13.bold("It should be there")} \u2014 write it into a migration so the history matches. Add the SQL to a`,
4564
+ " new migration and mark it applied with `prisma migrate resolve --applied <name>`.",
4565
+ "",
4566
+ ` ${pc13.bold("It should not be there")} \u2014 remove it from the database yourself, then run this again.`,
4567
+ "",
4568
+ "An index, a trigger or a function is not reported as drift at all; Prisma does not look at",
4569
+ "those. They survive a migration and are lost by a reset, which is the other reason not to."
4570
+ ].join("\n")
4571
+ );
4572
+ }
4573
+ return [];
4574
+ }
4575
+ const added = (existsSync20(migrations) ? readdirSync7(migrations) : []).filter((entry) => !before.has(entry));
4576
+ if (added.length === 0) {
4577
+ log2("No schema changes, so no migration was created.");
4578
+ return [];
4579
+ }
4580
+ const written = [];
4581
+ const destructive = [];
4582
+ for (const entry of added) {
4583
+ const file = join19(migrations, entry, "migration.sql");
4584
+ if (!existsSync20(file)) continue;
4585
+ const relative3 = `prisma/migrations/${entry}/migration.sql`;
4586
+ written.push(relative3);
4587
+ log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
4588
+ const found = destructiveStatements(readFileSync13(file, "utf8"));
4589
+ if (found.length > 0) destructive.push({ file: relative3, found });
4590
+ }
4591
+ if (destructive.length > 0) {
4592
+ log2("");
4593
+ log2(pc13.yellow(`This migration throws things away, so it has not been applied:`));
4594
+ for (const { file, found } of destructive) {
4595
+ log2(pc13.dim(`
4596
+ ${file}`));
4597
+ for (const entry of found) {
4598
+ log2(` ${pc13.red(`line ${entry.line}`)} ${entry.statement}`);
4599
+ log2(` ${pc13.dim(`drops ${entry.kind}`)}`);
4600
+ }
4601
+ }
4602
+ log2(
4603
+ [
4604
+ "",
4605
+ "There are two reasons Prisma writes a drop, and only you can tell them apart:",
4606
+ "",
4607
+ ` ${pc13.bold("You asked for it")} \u2014 a field left a descriptor, or a resource was removed. Then the drop is`,
4608
+ ` the point, and ${pc13.bold("flare migrate")} applies it.`,
4609
+ "",
4610
+ ` ${pc13.bold("Prisma cannot see it")} \u2014 the column, index or constraint was added by hand, so the schema`,
4611
+ " does not describe it and the diff reads it as something to remove. Then applying this",
4612
+ " loses it, and Prisma will not put it back.",
4613
+ "",
4614
+ `Read ${pc13.bold(destructive[0].file)} before deciding. To keep an object Prisma does not`,
4615
+ "know about, delete those statements from the migration \u2014 and consider writing the object into",
4616
+ "a migration of its own so the next diff stops proposing to drop it."
4617
+ ].join("\n")
4618
+ );
4619
+ return written;
4620
+ }
4621
+ log2(`
4622
+ Nothing destructive in it. Next: ${pc13.bold("flare migrate")} to apply it.`);
4623
+ return written;
4624
+ }
4355
4625
 
4356
4626
  // src/commands/gen-policy.ts
4357
4627
  import { existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync14 } from "fs";
@@ -4429,7 +4699,7 @@ ${pc14.dim("Roles come from `flare role:add`; the API and the admin both enforce
4429
4699
  }
4430
4700
 
4431
4701
  // src/commands/migrate.ts
4432
- import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4702
+ import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4433
4703
  import { tmpdir } from "os";
4434
4704
  import { join as join21 } from "path";
4435
4705
  import pc15 from "picocolors";
@@ -4570,7 +4840,7 @@ async function rollback(options2 = {}) {
4570
4840
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
4571
4841
  const handWritten = join21(migrationsDir, "down", name);
4572
4842
  if (existsSync22(handWritten)) {
4573
- const sql2 = readFileSync13(handWritten, "utf8");
4843
+ const sql2 = readFileSync14(handWritten, "utf8");
4574
4844
  if (splitStatements(sql2).length === 0) {
4575
4845
  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.`);
4576
4846
  }
@@ -4579,7 +4849,7 @@ async function rollback(options2 = {}) {
4579
4849
  }
4580
4850
  const upPath = join21(migrationsDir, name);
4581
4851
  if (!existsSync22(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
4582
- const down = deriveDown(readFileSync13(upPath, "utf8"));
4852
+ const down = deriveDown(readFileSync14(upPath, "utf8"));
4583
4853
  if (!down.reversible) {
4584
4854
  throw new Error(
4585
4855
  `Can't roll back ${name}: ${down.reason}
@@ -4615,7 +4885,7 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
4615
4885
  }
4616
4886
 
4617
4887
  // src/commands/rm.ts
4618
- import { existsSync as existsSync23, readdirSync as readdirSync7, readFileSync as readFileSync14, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
4888
+ import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
4619
4889
  import { dirname as dirname11, join as join22 } from "path";
4620
4890
  import { relationGraph as relationGraph3 } from "@flaredev/core";
4621
4891
  import pc16 from "picocolors";
@@ -4638,7 +4908,7 @@ function removeEmptyDirs(appRoot, start) {
4638
4908
  let dir = dirname11(join22(appRoot, start));
4639
4909
  const roots = [join22(appRoot, "app", "api"), join22(appRoot, "app", "dashboard")];
4640
4910
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
4641
- while (inside() && existsSync23(dir) && readdirSync7(dir).length === 0) {
4911
+ while (inside() && existsSync23(dir) && readdirSync8(dir).length === 0) {
4642
4912
  rmdirSync(dir);
4643
4913
  dir = dirname11(dir);
4644
4914
  }
@@ -4676,7 +4946,7 @@ async function rmResource(rawName, options2 = {}) {
4676
4946
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
4677
4947
  const findings = [];
4678
4948
  for (const path of owned) {
4679
- const source = readFileSync14(join22(appRoot, path), "utf8");
4949
+ const source = readFileSync15(join22(appRoot, path), "utf8");
4680
4950
  let wrapper;
4681
4951
  if (entry && path === entry.file) {
4682
4952
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -4704,8 +4974,8 @@ Move what you want to keep, or pass --force to delete anyway.`);
4704
4974
  logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
4705
4975
  const table = entry ? tableExport(entry.resource) : void 0;
4706
4976
  if (table && existsSync23(join22(appRoot, "seeds"))) {
4707
- const seeds = readdirSync7(join22(appRoot, "seeds")).filter(
4708
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync14(join22(appRoot, "seeds", file), "utf8"))
4977
+ const seeds = readdirSync8(join22(appRoot, "seeds")).filter(
4978
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync15(join22(appRoot, "seeds", file), "utf8"))
4709
4979
  );
4710
4980
  if (seeds.length) log2(pc16.yellow(`
4711
4981
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
@@ -4780,7 +5050,7 @@ ${result.output.trim()}`);
4780
5050
  }
4781
5051
 
4782
5052
  // src/commands/eject.ts
4783
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync8, writeFileSync as writeFileSync16 } from "fs";
5053
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync9, writeFileSync as writeFileSync16 } from "fs";
4784
5054
  import { dirname as dirname12, join as join23, relative as relative2 } from "path";
4785
5055
  import pc18 from "picocolors";
4786
5056
  var TRACKED = ["app", "lib", "components"];
@@ -4790,7 +5060,7 @@ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only)
4790
5060
  function upstreamFile(path, stack) {
4791
5061
  if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
4792
5062
  const found = locate(path, stack);
4793
- if (found && PER_APP.test(readFileSync15(found, "utf8"))) return void 0;
5063
+ if (found && PER_APP.test(readFileSync16(found, "utf8"))) return void 0;
4794
5064
  return found;
4795
5065
  }
4796
5066
  function locate(path, stack) {
@@ -4801,7 +5071,7 @@ function trackedPaths(appRoot, stack) {
4801
5071
  const paths = /* @__PURE__ */ new Set();
4802
5072
  const walk = (base, dir) => {
4803
5073
  if (!existsSync24(join23(base, dir))) return;
4804
- for (const entry of readdirSync8(join23(base, dir), { withFileTypes: true })) {
5074
+ for (const entry of readdirSync9(join23(base, dir), { withFileTypes: true })) {
4805
5075
  const next = `${dir}/${entry.name}`;
4806
5076
  if (entry.isDirectory()) walk(base, next);
4807
5077
  else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
@@ -4817,10 +5087,10 @@ function trackedPaths(appRoot, stack) {
4817
5087
  function compareTracked(appRoot, stack, filter) {
4818
5088
  return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
4819
5089
  const source = upstreamFile(path, stack);
4820
- const theirs = readFileSync15(source, "utf8");
5090
+ const theirs = readFileSync16(source, "utf8");
4821
5091
  const target = join23(appRoot, path);
4822
5092
  if (!existsSync24(target)) return { path, state: "missing", theirs };
4823
- const mine = readFileSync15(target, "utf8");
5093
+ const mine = readFileSync16(target, "utf8");
4824
5094
  if (mine === theirs) return { path, state: "same", mine, theirs };
4825
5095
  const merged = mergeGeneratedBlock(mine, theirs);
4826
5096
  if (merged !== void 0) {
@@ -4912,9 +5182,9 @@ Updated ${changed.length} file(s).`));
4912
5182
  }
4913
5183
 
4914
5184
  // src/commands/seed.ts
4915
- import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
5185
+ import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
4916
5186
  import { join as join25 } from "path";
4917
- import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
5187
+ import { camelCase as camelCase6, createFake, credentialUserRows, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
4918
5188
  import { createJiti as createJiti4 } from "jiti";
4919
5189
  import pc19 from "picocolors";
4920
5190
 
@@ -5010,7 +5280,7 @@ function createInsertMany(sink, drizzle, onProgress) {
5010
5280
  }
5011
5281
 
5012
5282
  // src/seed/local-d1.ts
5013
- import { readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
5283
+ import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
5014
5284
  import { createRequire } from "module";
5015
5285
  import { join as join24 } from "path";
5016
5286
  import { pathToFileURL } from "url";
@@ -5043,7 +5313,7 @@ async function openLocalD1(appRoot, binding) {
5043
5313
  }
5044
5314
  function seedConfig(appRoot) {
5045
5315
  const source = join24(appRoot, "wrangler.jsonc");
5046
- const config = parseJsonc3(readFileSync16(source, "utf8")) ?? {};
5316
+ const config = parseJsonc3(readFileSync17(source, "utf8")) ?? {};
5047
5317
  delete config.durable_objects;
5048
5318
  delete config.migrations;
5049
5319
  const path = join24(appRoot, ".flare-seed.wrangler.json");
@@ -5055,7 +5325,7 @@ function seedConfig(appRoot) {
5055
5325
  var SEEDS_DIR = "seeds";
5056
5326
  function seedFiles(appRoot, names) {
5057
5327
  const dir = join25(appRoot, SEEDS_DIR);
5058
- const all = existsSync25(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
5328
+ const all = existsSync25(dir) ? readdirSync10(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
5059
5329
  if (!names?.length) return all;
5060
5330
  return names.map((name) => {
5061
5331
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -5085,11 +5355,12 @@ async function runSeeds(options2 = {}) {
5085
5355
  try {
5086
5356
  const db = drizzle(local.db, { schema });
5087
5357
  const insertMany = createInsertMany(local.db, drizzleHelpers);
5358
+ const seedUser = drizzleSeedUser(db, schema);
5088
5359
  for (const file of files) {
5089
5360
  const mod = await jiti.import(join25(appRoot, SEEDS_DIR, file));
5090
5361
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
5091
5362
  log2(`${pc19.cyan("seed")} ${file}`);
5092
- await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
5363
+ await mod.default({ db, env: local.env, insertMany, seedUser, fake: createFake(), log: (message) => log2(` ${message}`) });
5093
5364
  }
5094
5365
  log2(pc19.green(`
5095
5366
  Ran ${files.length} seed(s) against the local database.`));
@@ -5097,6 +5368,17 @@ Ran ${files.length} seed(s) against the local database.`));
5097
5368
  await local.dispose();
5098
5369
  }
5099
5370
  }
5371
+ function drizzleSeedUser(db, schema) {
5372
+ return async (input) => {
5373
+ const user = schema.user;
5374
+ const account = schema.account;
5375
+ if (!user || !account) throw new Error("db/schema.ts has no `user` and `account` tables, so there is nothing to sign in against.");
5376
+ const rows = await credentialUserRows(input);
5377
+ await db.insert(user).values(rows.user);
5378
+ await db.insert(account).values(rows.account);
5379
+ return rows.user;
5380
+ };
5381
+ }
5100
5382
  function sampleValue(key, def) {
5101
5383
  const name = key.toLowerCase();
5102
5384
  switch (def.kind) {
@@ -5144,18 +5426,22 @@ function renderSeed(resource, stack = "cloudflare") {
5144
5426
  if (!resource) {
5145
5427
  return stack === "next" ? `import { defineSeed } from "@flaredev/core";
5146
5428
 
5147
- export default defineSeed(async ({ db, insertMany, fake, log }) => {
5429
+ export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
5148
5430
  // \`db\` is this app's Prisma client, e.g.:
5149
5431
  // await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
5150
5432
  // Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
5433
+ // Somebody you can sign in as, with the credential account Better Auth looks for:
5434
+ // await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
5151
5435
  log("nothing to seed yet");
5152
5436
  });
5153
5437
  ` : `import { defineSeed } from "@flaredev/core";
5154
5438
 
5155
- export default defineSeed(async ({ db, insertMany, fake, log }) => {
5439
+ export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
5156
5440
  // Insert rows with Drizzle, e.g.:
5157
5441
  // await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
5158
5442
  // Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
5443
+ // Somebody you can sign in as, with the credential account Better Auth looks for:
5444
+ // await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
5159
5445
  log("nothing to seed yet");
5160
5446
  });
5161
5447
  `;
@@ -5225,10 +5511,17 @@ Run it with ${pc19.bold(`flare seed ${name}`)}.`);
5225
5511
  return relative3;
5226
5512
  }
5227
5513
  async function runPrismaSeeds(appRoot, files, log2) {
5514
+ if (!existsSync25(join25(appRoot, "lib", "generated", "prisma"))) {
5515
+ log2(pc19.dim("Generating the Prisma client first (migrate dev no longer does)\u2026"));
5516
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
5517
+ if (await runNode([prisma, "generate"], appRoot) !== 0) {
5518
+ throw new Error("`prisma generate` failed, so there is no client for the seeds to use.");
5519
+ }
5520
+ }
5228
5521
  const runner = join25(appRoot, ".flare-seed-runner.mts");
5229
5522
  const script = [
5230
5523
  `import "dotenv/config";`,
5231
- `import { createFake } from "@flaredev/core";`,
5524
+ `import { createFake, credentialUserRows } from "@flaredev/core";`,
5232
5525
  `import { prisma } from "./lib/db";`,
5233
5526
  ...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
5234
5527
  "",
@@ -5246,6 +5539,13 @@ async function runPrismaSeeds(appRoot, files, log2) {
5246
5539
  ` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
5247
5540
  ` await delegate.createMany({ data: rows });`,
5248
5541
  ` },`,
5542
+ ` seedUser: async (input: Parameters<typeof credentialUserRows>[0]) => {`,
5543
+ ` const rows = await credentialUserRows(input);`,
5544
+ ` const models = prisma as unknown as Record<string, { create(args: { data: unknown }): Promise<unknown> }>;`,
5545
+ ` await models.user.create({ data: rows.user });`,
5546
+ ` await models.account.create({ data: rows.account });`,
5547
+ ` return rows.user;`,
5548
+ ` },`,
5249
5549
  ` fake: createFake(),`,
5250
5550
  ` log: (message: string) => console.log(" " + message),`,
5251
5551
  ` });`,
@@ -5381,7 +5681,7 @@ import { join as join27 } from "path";
5381
5681
  import pc21 from "picocolors";
5382
5682
 
5383
5683
  // src/seed/rows.ts
5384
- import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
5684
+ import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4, toMinorUnits } from "@flaredev/core";
5385
5685
  var has = (key, ...words) => words.some((word) => key.includes(word));
5386
5686
  function stringValue(key, def, fake, resource, index) {
5387
5687
  switch (def.format) {
@@ -5435,7 +5735,8 @@ function numberValue(key, def, fake) {
5435
5735
  min = min ?? low;
5436
5736
  max = max ?? high;
5437
5737
  }
5438
- return def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, money ? 2 : 2);
5738
+ const amount = def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, 2);
5739
+ return def.format === "money" ? toMinorUnits(amount, def.currency) : amount;
5439
5740
  }
5440
5741
  function fieldValue(key, def, fake, resource, parentIds, index) {
5441
5742
  const name = key.toLowerCase();
@@ -5665,7 +5966,7 @@ ${result.output.trim()}`);
5665
5966
 
5666
5967
  // src/commands/billing-sync.ts
5667
5968
  import { createRequire as createRequire2 } from "module";
5668
- import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync21 } from "fs";
5969
+ import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync21 } from "fs";
5669
5970
  import { tmpdir as tmpdir4 } from "os";
5670
5971
  import { join as join28 } from "path";
5671
5972
  import { pathToFileURL as pathToFileURL2 } from "url";
@@ -5675,7 +5976,7 @@ function stripeKeys(appRoot) {
5675
5976
  const map = {};
5676
5977
  const path = join28(appRoot, ".dev.vars");
5677
5978
  if (existsSync26(path)) {
5678
- for (const line of readFileSync17(path, "utf8").split(/\r?\n/)) {
5979
+ for (const line of readFileSync18(path, "utf8").split(/\r?\n/)) {
5679
5980
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
5680
5981
  if (match) map[match[1]] = match[2].trim();
5681
5982
  }
@@ -5761,7 +6062,7 @@ async function syncPlans(options2 = {}) {
5761
6062
  const appRequire = createRequire2(join28(appRoot, "package.json"));
5762
6063
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
5763
6064
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
5764
- const appName = JSON.parse(readFileSync17(join28(appRoot, "package.json"), "utf8")).name ?? "app";
6065
+ const appName = JSON.parse(readFileSync18(join28(appRoot, "package.json"), "utf8")).name ?? "app";
5765
6066
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
5766
6067
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
5767
6068
  if (products.length === 0) {
@@ -5936,13 +6237,13 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
5936
6237
 
5937
6238
  // src/utils/install.ts
5938
6239
  import { spawn as spawn5 } from "child_process";
5939
- import { existsSync as existsSync28, readFileSync as readFileSync18, readdirSync as readdirSync10 } from "fs";
6240
+ import { existsSync as existsSync28, readFileSync as readFileSync19, readdirSync as readdirSync11 } from "fs";
5940
6241
  import { join as join31 } from "path";
5941
6242
  function expectedPackages(dir) {
5942
6243
  const npmLock = join31(dir, "package-lock.json");
5943
6244
  if (existsSync28(npmLock)) {
5944
6245
  try {
5945
- const lock = JSON.parse(readFileSync18(npmLock, "utf8"));
6246
+ const lock = JSON.parse(readFileSync19(npmLock, "utf8"));
5946
6247
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
5947
6248
  } catch {
5948
6249
  return 0;
@@ -5951,7 +6252,7 @@ function expectedPackages(dir) {
5951
6252
  const pnpmLock = join31(dir, "pnpm-lock.yaml");
5952
6253
  if (existsSync28(pnpmLock)) {
5953
6254
  try {
5954
- const text = readFileSync18(pnpmLock, "utf8");
6255
+ const text = readFileSync19(pnpmLock, "utf8");
5955
6256
  const packages = text.slice(text.indexOf("\npackages:"));
5956
6257
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
5957
6258
  } catch {
@@ -5965,11 +6266,11 @@ function installedPackages(dir) {
5965
6266
  if (!existsSync28(root)) return 0;
5966
6267
  let total = 0;
5967
6268
  try {
5968
- for (const entry of readdirSync10(root, { withFileTypes: true })) {
6269
+ for (const entry of readdirSync11(root, { withFileTypes: true })) {
5969
6270
  if (!entry.isDirectory() || entry.name === ".bin") continue;
5970
6271
  if (entry.name.startsWith("@")) {
5971
6272
  try {
5972
- total += readdirSync10(join31(root, entry.name)).length;
6273
+ total += readdirSync11(join31(root, entry.name)).length;
5973
6274
  } catch {
5974
6275
  }
5975
6276
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -6024,7 +6325,7 @@ function createCli() {
6024
6325
  const stack = currentStack();
6025
6326
  const perStack = (cloudflare, next, either) => stack === "next" ? next : stack === "cloudflare" ? cloudflare : either;
6026
6327
  const cloudflareOnly = (text) => stack === void 0 ? `${text} (Cloudflare only)` : text;
6027
- 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(
6328
+ 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: next (default) or cloudflare").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(
6028
6329
  async (dir, options2) => {
6029
6330
  const wanted = !options2.skipInstall;
6030
6331
  let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
@@ -6051,7 +6352,7 @@ function createCli() {
6051
6352
  printNextSteps(result, installed);
6052
6353
  }
6053
6354
  );
6054
- cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).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("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").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("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").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 policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
6355
+ cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).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("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").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 (Cloudflare only)").example("flare gen migration add_phone --from-schema # Cloudflare: diff the Drizzle tables").example("flare gen migration add_phone # Next.js: prisma migrate dev, refusing a drop").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("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").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 policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
6055
6356
  async (generator, name, second, options2) => {
6056
6357
  if (generator === "billing")
6057
6358
  return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });