@flaredev/cli 0.9.0 → 0.10.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
@@ -189,7 +189,11 @@ 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",
@@ -1275,6 +1279,20 @@ function runNode(argv, cwd) {
1275
1279
  child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
1276
1280
  });
1277
1281
  }
1282
+ function runNodeCapturing(argv, cwd) {
1283
+ return new Promise((resolvePromise, reject) => {
1284
+ const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"] });
1285
+ let output = "";
1286
+ for (const stream of [child.stdout, child.stderr]) {
1287
+ stream?.on("data", (chunk) => {
1288
+ output += chunk.toString();
1289
+ process.stdout.write(chunk);
1290
+ });
1291
+ }
1292
+ child.on("error", reject);
1293
+ child.on("exit", (code, signal) => resolvePromise({ code: code ?? (signal ? 1 : 0), output }));
1294
+ });
1295
+ }
1278
1296
  function runNodeWithTunnel(argv, cwd, opened) {
1279
1297
  return new Promise((resolvePromise, reject) => {
1280
1298
  const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
@@ -2644,7 +2662,7 @@ function prismaType(resource, key, def) {
2644
2662
  case "int":
2645
2663
  return "Int";
2646
2664
  case "float":
2647
- return "Float";
2665
+ return def.format === "money" ? "BigInt" : "Float";
2648
2666
  case "boolean":
2649
2667
  return "Boolean";
2650
2668
  case "date":
@@ -2773,7 +2791,7 @@ function columnExpression(key, def, resources, selfName) {
2773
2791
  expr = `integer(${col})`;
2774
2792
  break;
2775
2793
  case "float":
2776
- expr = `real(${col})`;
2794
+ expr = def.format === "money" ? `integer(${col})` : `real(${col})`;
2777
2795
  break;
2778
2796
  case "boolean":
2779
2797
  expr = `integer(${col}, { mode: "boolean" })`;
@@ -4314,10 +4332,88 @@ ${body}
4314
4332
  }
4315
4333
 
4316
4334
  // src/commands/gen-migration.ts
4317
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
4335
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, writeFileSync as writeFileSync13 } from "fs";
4318
4336
  import { basename as basename2, join as join19 } from "path";
4319
4337
  import { snakeCase } from "@flaredev/core";
4320
4338
  import pc13 from "picocolors";
4339
+
4340
+ // src/generator/destructive.ts
4341
+ function stripNoise(sql2) {
4342
+ let out = "";
4343
+ let index = 0;
4344
+ while (index < sql2.length) {
4345
+ const two = sql2.slice(index, index + 2);
4346
+ if (two === "--") {
4347
+ const end = sql2.indexOf("\n", index);
4348
+ const stop = end === -1 ? sql2.length : end;
4349
+ out += " ".repeat(stop - index);
4350
+ index = stop;
4351
+ continue;
4352
+ }
4353
+ if (two === "/*") {
4354
+ const end = sql2.indexOf("*/", index + 2);
4355
+ const skipped = sql2.slice(index, end === -1 ? sql2.length : end + 2);
4356
+ out += skipped.replaceAll(/[^\n]/g, " ");
4357
+ index = end === -1 ? sql2.length : end + 2;
4358
+ continue;
4359
+ }
4360
+ const char = sql2[index];
4361
+ if (char === "'" || char === '"' || char === "`") {
4362
+ let at = index + 1;
4363
+ while (at < sql2.length) {
4364
+ if (sql2[at] === char) {
4365
+ if (sql2[at + 1] === char) at += 2;
4366
+ else break;
4367
+ } else at += 1;
4368
+ }
4369
+ const literal2 = sql2.slice(index, Math.min(at + 1, sql2.length));
4370
+ out += literal2.replaceAll(/[^\n]/g, " ");
4371
+ index = at + 1;
4372
+ continue;
4373
+ }
4374
+ out += char;
4375
+ index += 1;
4376
+ }
4377
+ return out;
4378
+ }
4379
+ var PATTERNS = [
4380
+ [/\bDROP\s+COLUMN\b/i, "a column, and the values in it"],
4381
+ [/\bDROP\s+TABLE\b/i, "a table, and every row in it"],
4382
+ [/\bTRUNCATE\b/i, "every row in a table"],
4383
+ [/\bDROP\s+(?:MATERIALIZED\s+)?VIEW\b/i, "a view"],
4384
+ [/\bDROP\s+INDEX\b/i, "an index \u2014 if it was written by hand, Prisma cannot recreate it"],
4385
+ [/\bDROP\s+TRIGGER\b/i, "a trigger \u2014 Prisma does not know about it and will not put it back"],
4386
+ [/\bDROP\s+FUNCTION\b/i, "a function"],
4387
+ [/\bDROP\s+CONSTRAINT\b/i, "a constraint"],
4388
+ [/\bDROP\s+TYPE\b/i, "a type"],
4389
+ [/\bDROP\s+SEQUENCE\b/i, "a sequence"],
4390
+ [/\bDROP\s+SCHEMA\b/i, "a schema, and everything in it"],
4391
+ [/\bDROP\s+DATABASE\b/i, "the database"]
4392
+ ];
4393
+ function destructiveStatements(sql2) {
4394
+ const cleaned = stripNoise(sql2);
4395
+ const found = [];
4396
+ let start = 0;
4397
+ for (let index = 0; index <= cleaned.length; index += 1) {
4398
+ if (index !== cleaned.length && cleaned[index] !== ";") continue;
4399
+ const statement = cleaned.slice(start, index);
4400
+ const trimmed = statement.trim();
4401
+ const from = start;
4402
+ start = index + 1;
4403
+ if (!trimmed) continue;
4404
+ const match = PATTERNS.find(([pattern]) => pattern.test(trimmed));
4405
+ if (!match) continue;
4406
+ const offset = from + statement.search(/\S/);
4407
+ found.push({
4408
+ statement: sql2.slice(offset, index).trim().replaceAll(/\s+/g, " ").slice(0, 160),
4409
+ kind: match[1],
4410
+ line: cleaned.slice(0, offset).split("\n").length
4411
+ });
4412
+ }
4413
+ return found;
4414
+ }
4415
+
4416
+ // src/commands/gen-migration.ts
4321
4417
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
4322
4418
  -- Write SQL that undoes the up migration, e.g.:
4323
4419
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
@@ -4328,6 +4424,7 @@ async function genMigration(rawName, options2 = {}) {
4328
4424
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4329
4425
  const name = snakeCase(rawName);
4330
4426
  if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
4427
+ if (readStack(appRoot) === "next") return genPrismaMigration(appRoot, name, options2);
4331
4428
  const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
4332
4429
  if (files.length === 0) {
4333
4430
  log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
@@ -4352,6 +4449,89 @@ Next: ${pc13.bold("flare migrate")}.`);
4352
4449
  }
4353
4450
  return files;
4354
4451
  }
4452
+ async function genPrismaMigration(appRoot, name, options2) {
4453
+ const log2 = options2.log ?? ((message) => console.log(message));
4454
+ const prisma = resolveBin(appRoot, "prisma", "prisma");
4455
+ const migrations = join19(appRoot, "prisma", "migrations");
4456
+ const before = new Set(existsSync20(migrations) ? readdirSync7(migrations) : []);
4457
+ log2(`${pc13.cyan("prisma")} migrate dev --create-only --name ${name}`);
4458
+ const { code, output } = await runNodeCapturing([prisma, "migrate", "dev", "--create-only", "--name", name], appRoot);
4459
+ if (code !== 0) {
4460
+ if (/drift detected|we need to reset/i.test(output)) {
4461
+ log2("");
4462
+ log2(pc13.yellow("Prisma found something in the database that its schema does not describe."));
4463
+ log2(
4464
+ [
4465
+ "",
4466
+ `Nothing has been changed \u2014 ${pc13.bold("--create-only")} writes a migration, it does not apply one, and no`,
4467
+ "migration was written. Do not run `prisma migrate reset`: it drops the database, and what it",
4468
+ "would drop is the thing Prisma is complaining it cannot see.",
4469
+ "",
4470
+ "Drift above is reported as changes to get from your migrations to the database, so it lists",
4471
+ "what the database has and the migrations do not. Usually that is something added by hand:",
4472
+ "",
4473
+ ` ${pc13.bold("It should be there")} \u2014 write it into a migration so the history matches. Add the SQL to a`,
4474
+ " new migration and mark it applied with `prisma migrate resolve --applied <name>`.",
4475
+ "",
4476
+ ` ${pc13.bold("It should not be there")} \u2014 remove it from the database yourself, then run this again.`,
4477
+ "",
4478
+ "An index, a trigger or a function is not reported as drift at all; Prisma does not look at",
4479
+ "those. They survive a migration and are lost by a reset, which is the other reason not to."
4480
+ ].join("\n")
4481
+ );
4482
+ }
4483
+ return [];
4484
+ }
4485
+ const added = (existsSync20(migrations) ? readdirSync7(migrations) : []).filter((entry) => !before.has(entry));
4486
+ if (added.length === 0) {
4487
+ log2("No schema changes, so no migration was created.");
4488
+ return [];
4489
+ }
4490
+ const written = [];
4491
+ const destructive = [];
4492
+ for (const entry of added) {
4493
+ const file = join19(migrations, entry, "migration.sql");
4494
+ if (!existsSync20(file)) continue;
4495
+ const relative3 = `prisma/migrations/${entry}/migration.sql`;
4496
+ written.push(relative3);
4497
+ log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
4498
+ const found = destructiveStatements(readFileSync13(file, "utf8"));
4499
+ if (found.length > 0) destructive.push({ file: relative3, found });
4500
+ }
4501
+ if (destructive.length > 0) {
4502
+ log2("");
4503
+ log2(pc13.yellow(`This migration throws things away, so it has not been applied:`));
4504
+ for (const { file, found } of destructive) {
4505
+ log2(pc13.dim(`
4506
+ ${file}`));
4507
+ for (const entry of found) {
4508
+ log2(` ${pc13.red(`line ${entry.line}`)} ${entry.statement}`);
4509
+ log2(` ${pc13.dim(`drops ${entry.kind}`)}`);
4510
+ }
4511
+ }
4512
+ log2(
4513
+ [
4514
+ "",
4515
+ "There are two reasons Prisma writes a drop, and only you can tell them apart:",
4516
+ "",
4517
+ ` ${pc13.bold("You asked for it")} \u2014 a field left a descriptor, or a resource was removed. Then the drop is`,
4518
+ ` the point, and ${pc13.bold("flare migrate")} applies it.`,
4519
+ "",
4520
+ ` ${pc13.bold("Prisma cannot see it")} \u2014 the column, index or constraint was added by hand, so the schema`,
4521
+ " does not describe it and the diff reads it as something to remove. Then applying this",
4522
+ " loses it, and Prisma will not put it back.",
4523
+ "",
4524
+ `Read ${pc13.bold(destructive[0].file)} before deciding. To keep an object Prisma does not`,
4525
+ "know about, delete those statements from the migration \u2014 and consider writing the object into",
4526
+ "a migration of its own so the next diff stops proposing to drop it."
4527
+ ].join("\n")
4528
+ );
4529
+ return written;
4530
+ }
4531
+ log2(`
4532
+ Nothing destructive in it. Next: ${pc13.bold("flare migrate")} to apply it.`);
4533
+ return written;
4534
+ }
4355
4535
 
4356
4536
  // src/commands/gen-policy.ts
4357
4537
  import { existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync14 } from "fs";
@@ -4429,7 +4609,7 @@ ${pc14.dim("Roles come from `flare role:add`; the API and the admin both enforce
4429
4609
  }
4430
4610
 
4431
4611
  // src/commands/migrate.ts
4432
- import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4612
+ import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
4433
4613
  import { tmpdir } from "os";
4434
4614
  import { join as join21 } from "path";
4435
4615
  import pc15 from "picocolors";
@@ -4570,7 +4750,7 @@ async function rollback(options2 = {}) {
4570
4750
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
4571
4751
  const handWritten = join21(migrationsDir, "down", name);
4572
4752
  if (existsSync22(handWritten)) {
4573
- const sql2 = readFileSync13(handWritten, "utf8");
4753
+ const sql2 = readFileSync14(handWritten, "utf8");
4574
4754
  if (splitStatements(sql2).length === 0) {
4575
4755
  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
4756
  }
@@ -4579,7 +4759,7 @@ async function rollback(options2 = {}) {
4579
4759
  }
4580
4760
  const upPath = join21(migrationsDir, name);
4581
4761
  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"));
4762
+ const down = deriveDown(readFileSync14(upPath, "utf8"));
4583
4763
  if (!down.reversible) {
4584
4764
  throw new Error(
4585
4765
  `Can't roll back ${name}: ${down.reason}
@@ -4615,7 +4795,7 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
4615
4795
  }
4616
4796
 
4617
4797
  // src/commands/rm.ts
4618
- import { existsSync as existsSync23, readdirSync as readdirSync7, readFileSync as readFileSync14, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
4798
+ import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
4619
4799
  import { dirname as dirname11, join as join22 } from "path";
4620
4800
  import { relationGraph as relationGraph3 } from "@flaredev/core";
4621
4801
  import pc16 from "picocolors";
@@ -4638,7 +4818,7 @@ function removeEmptyDirs(appRoot, start) {
4638
4818
  let dir = dirname11(join22(appRoot, start));
4639
4819
  const roots = [join22(appRoot, "app", "api"), join22(appRoot, "app", "dashboard")];
4640
4820
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
4641
- while (inside() && existsSync23(dir) && readdirSync7(dir).length === 0) {
4821
+ while (inside() && existsSync23(dir) && readdirSync8(dir).length === 0) {
4642
4822
  rmdirSync(dir);
4643
4823
  dir = dirname11(dir);
4644
4824
  }
@@ -4676,7 +4856,7 @@ async function rmResource(rawName, options2 = {}) {
4676
4856
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
4677
4857
  const findings = [];
4678
4858
  for (const path of owned) {
4679
- const source = readFileSync14(join22(appRoot, path), "utf8");
4859
+ const source = readFileSync15(join22(appRoot, path), "utf8");
4680
4860
  let wrapper;
4681
4861
  if (entry && path === entry.file) {
4682
4862
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -4704,8 +4884,8 @@ Move what you want to keep, or pass --force to delete anyway.`);
4704
4884
  logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
4705
4885
  const table = entry ? tableExport(entry.resource) : void 0;
4706
4886
  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"))
4887
+ const seeds = readdirSync8(join22(appRoot, "seeds")).filter(
4888
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync15(join22(appRoot, "seeds", file), "utf8"))
4709
4889
  );
4710
4890
  if (seeds.length) log2(pc16.yellow(`
4711
4891
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
@@ -4780,7 +4960,7 @@ ${result.output.trim()}`);
4780
4960
  }
4781
4961
 
4782
4962
  // src/commands/eject.ts
4783
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync8, writeFileSync as writeFileSync16 } from "fs";
4963
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync9, writeFileSync as writeFileSync16 } from "fs";
4784
4964
  import { dirname as dirname12, join as join23, relative as relative2 } from "path";
4785
4965
  import pc18 from "picocolors";
4786
4966
  var TRACKED = ["app", "lib", "components"];
@@ -4790,7 +4970,7 @@ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only)
4790
4970
  function upstreamFile(path, stack) {
4791
4971
  if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
4792
4972
  const found = locate(path, stack);
4793
- if (found && PER_APP.test(readFileSync15(found, "utf8"))) return void 0;
4973
+ if (found && PER_APP.test(readFileSync16(found, "utf8"))) return void 0;
4794
4974
  return found;
4795
4975
  }
4796
4976
  function locate(path, stack) {
@@ -4801,7 +4981,7 @@ function trackedPaths(appRoot, stack) {
4801
4981
  const paths = /* @__PURE__ */ new Set();
4802
4982
  const walk = (base, dir) => {
4803
4983
  if (!existsSync24(join23(base, dir))) return;
4804
- for (const entry of readdirSync8(join23(base, dir), { withFileTypes: true })) {
4984
+ for (const entry of readdirSync9(join23(base, dir), { withFileTypes: true })) {
4805
4985
  const next = `${dir}/${entry.name}`;
4806
4986
  if (entry.isDirectory()) walk(base, next);
4807
4987
  else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
@@ -4817,10 +4997,10 @@ function trackedPaths(appRoot, stack) {
4817
4997
  function compareTracked(appRoot, stack, filter) {
4818
4998
  return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
4819
4999
  const source = upstreamFile(path, stack);
4820
- const theirs = readFileSync15(source, "utf8");
5000
+ const theirs = readFileSync16(source, "utf8");
4821
5001
  const target = join23(appRoot, path);
4822
5002
  if (!existsSync24(target)) return { path, state: "missing", theirs };
4823
- const mine = readFileSync15(target, "utf8");
5003
+ const mine = readFileSync16(target, "utf8");
4824
5004
  if (mine === theirs) return { path, state: "same", mine, theirs };
4825
5005
  const merged = mergeGeneratedBlock(mine, theirs);
4826
5006
  if (merged !== void 0) {
@@ -4912,9 +5092,9 @@ Updated ${changed.length} file(s).`));
4912
5092
  }
4913
5093
 
4914
5094
  // src/commands/seed.ts
4915
- import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
5095
+ import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
4916
5096
  import { join as join25 } from "path";
4917
- import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
5097
+ import { camelCase as camelCase6, createFake, credentialUserRows, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
4918
5098
  import { createJiti as createJiti4 } from "jiti";
4919
5099
  import pc19 from "picocolors";
4920
5100
 
@@ -5010,7 +5190,7 @@ function createInsertMany(sink, drizzle, onProgress) {
5010
5190
  }
5011
5191
 
5012
5192
  // src/seed/local-d1.ts
5013
- import { readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
5193
+ import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
5014
5194
  import { createRequire } from "module";
5015
5195
  import { join as join24 } from "path";
5016
5196
  import { pathToFileURL } from "url";
@@ -5043,7 +5223,7 @@ async function openLocalD1(appRoot, binding) {
5043
5223
  }
5044
5224
  function seedConfig(appRoot) {
5045
5225
  const source = join24(appRoot, "wrangler.jsonc");
5046
- const config = parseJsonc3(readFileSync16(source, "utf8")) ?? {};
5226
+ const config = parseJsonc3(readFileSync17(source, "utf8")) ?? {};
5047
5227
  delete config.durable_objects;
5048
5228
  delete config.migrations;
5049
5229
  const path = join24(appRoot, ".flare-seed.wrangler.json");
@@ -5055,7 +5235,7 @@ function seedConfig(appRoot) {
5055
5235
  var SEEDS_DIR = "seeds";
5056
5236
  function seedFiles(appRoot, names) {
5057
5237
  const dir = join25(appRoot, SEEDS_DIR);
5058
- const all = existsSync25(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
5238
+ const all = existsSync25(dir) ? readdirSync10(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
5059
5239
  if (!names?.length) return all;
5060
5240
  return names.map((name) => {
5061
5241
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -5085,11 +5265,12 @@ async function runSeeds(options2 = {}) {
5085
5265
  try {
5086
5266
  const db = drizzle(local.db, { schema });
5087
5267
  const insertMany = createInsertMany(local.db, drizzleHelpers);
5268
+ const seedUser = drizzleSeedUser(db, schema);
5088
5269
  for (const file of files) {
5089
5270
  const mod = await jiti.import(join25(appRoot, SEEDS_DIR, file));
5090
5271
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
5091
5272
  log2(`${pc19.cyan("seed")} ${file}`);
5092
- await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
5273
+ await mod.default({ db, env: local.env, insertMany, seedUser, fake: createFake(), log: (message) => log2(` ${message}`) });
5093
5274
  }
5094
5275
  log2(pc19.green(`
5095
5276
  Ran ${files.length} seed(s) against the local database.`));
@@ -5097,6 +5278,17 @@ Ran ${files.length} seed(s) against the local database.`));
5097
5278
  await local.dispose();
5098
5279
  }
5099
5280
  }
5281
+ function drizzleSeedUser(db, schema) {
5282
+ return async (input) => {
5283
+ const user = schema.user;
5284
+ const account = schema.account;
5285
+ if (!user || !account) throw new Error("db/schema.ts has no `user` and `account` tables, so there is nothing to sign in against.");
5286
+ const rows = await credentialUserRows(input);
5287
+ await db.insert(user).values(rows.user);
5288
+ await db.insert(account).values(rows.account);
5289
+ return rows.user;
5290
+ };
5291
+ }
5100
5292
  function sampleValue(key, def) {
5101
5293
  const name = key.toLowerCase();
5102
5294
  switch (def.kind) {
@@ -5144,18 +5336,22 @@ function renderSeed(resource, stack = "cloudflare") {
5144
5336
  if (!resource) {
5145
5337
  return stack === "next" ? `import { defineSeed } from "@flaredev/core";
5146
5338
 
5147
- export default defineSeed(async ({ db, insertMany, fake, log }) => {
5339
+ export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
5148
5340
  // \`db\` is this app's Prisma client, e.g.:
5149
5341
  // await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
5150
5342
  // Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
5343
+ // Somebody you can sign in as, with the credential account Better Auth looks for:
5344
+ // await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
5151
5345
  log("nothing to seed yet");
5152
5346
  });
5153
5347
  ` : `import { defineSeed } from "@flaredev/core";
5154
5348
 
5155
- export default defineSeed(async ({ db, insertMany, fake, log }) => {
5349
+ export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
5156
5350
  // Insert rows with Drizzle, e.g.:
5157
5351
  // await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
5158
5352
  // Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
5353
+ // Somebody you can sign in as, with the credential account Better Auth looks for:
5354
+ // await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
5159
5355
  log("nothing to seed yet");
5160
5356
  });
5161
5357
  `;
@@ -5228,7 +5424,7 @@ async function runPrismaSeeds(appRoot, files, log2) {
5228
5424
  const runner = join25(appRoot, ".flare-seed-runner.mts");
5229
5425
  const script = [
5230
5426
  `import "dotenv/config";`,
5231
- `import { createFake } from "@flaredev/core";`,
5427
+ `import { createFake, credentialUserRows } from "@flaredev/core";`,
5232
5428
  `import { prisma } from "./lib/db";`,
5233
5429
  ...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
5234
5430
  "",
@@ -5246,6 +5442,13 @@ async function runPrismaSeeds(appRoot, files, log2) {
5246
5442
  ` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
5247
5443
  ` await delegate.createMany({ data: rows });`,
5248
5444
  ` },`,
5445
+ ` seedUser: async (input: Parameters<typeof credentialUserRows>[0]) => {`,
5446
+ ` const rows = await credentialUserRows(input);`,
5447
+ ` const models = prisma as unknown as Record<string, { create(args: { data: unknown }): Promise<unknown> }>;`,
5448
+ ` await models.user.create({ data: rows.user });`,
5449
+ ` await models.account.create({ data: rows.account });`,
5450
+ ` return rows.user;`,
5451
+ ` },`,
5249
5452
  ` fake: createFake(),`,
5250
5453
  ` log: (message: string) => console.log(" " + message),`,
5251
5454
  ` });`,
@@ -5381,7 +5584,7 @@ import { join as join27 } from "path";
5381
5584
  import pc21 from "picocolors";
5382
5585
 
5383
5586
  // src/seed/rows.ts
5384
- import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
5587
+ import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4, toMinorUnits } from "@flaredev/core";
5385
5588
  var has = (key, ...words) => words.some((word) => key.includes(word));
5386
5589
  function stringValue(key, def, fake, resource, index) {
5387
5590
  switch (def.format) {
@@ -5435,7 +5638,8 @@ function numberValue(key, def, fake) {
5435
5638
  min = min ?? low;
5436
5639
  max = max ?? high;
5437
5640
  }
5438
- return def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, money ? 2 : 2);
5641
+ const amount = def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, 2);
5642
+ return def.format === "money" ? toMinorUnits(amount, def.currency) : amount;
5439
5643
  }
5440
5644
  function fieldValue(key, def, fake, resource, parentIds, index) {
5441
5645
  const name = key.toLowerCase();
@@ -5665,7 +5869,7 @@ ${result.output.trim()}`);
5665
5869
 
5666
5870
  // src/commands/billing-sync.ts
5667
5871
  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";
5872
+ import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync21 } from "fs";
5669
5873
  import { tmpdir as tmpdir4 } from "os";
5670
5874
  import { join as join28 } from "path";
5671
5875
  import { pathToFileURL as pathToFileURL2 } from "url";
@@ -5675,7 +5879,7 @@ function stripeKeys(appRoot) {
5675
5879
  const map = {};
5676
5880
  const path = join28(appRoot, ".dev.vars");
5677
5881
  if (existsSync26(path)) {
5678
- for (const line of readFileSync17(path, "utf8").split(/\r?\n/)) {
5882
+ for (const line of readFileSync18(path, "utf8").split(/\r?\n/)) {
5679
5883
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
5680
5884
  if (match) map[match[1]] = match[2].trim();
5681
5885
  }
@@ -5761,7 +5965,7 @@ async function syncPlans(options2 = {}) {
5761
5965
  const appRequire = createRequire2(join28(appRoot, "package.json"));
5762
5966
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
5763
5967
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
5764
- const appName = JSON.parse(readFileSync17(join28(appRoot, "package.json"), "utf8")).name ?? "app";
5968
+ const appName = JSON.parse(readFileSync18(join28(appRoot, "package.json"), "utf8")).name ?? "app";
5765
5969
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
5766
5970
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
5767
5971
  if (products.length === 0) {
@@ -5936,13 +6140,13 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
5936
6140
 
5937
6141
  // src/utils/install.ts
5938
6142
  import { spawn as spawn5 } from "child_process";
5939
- import { existsSync as existsSync28, readFileSync as readFileSync18, readdirSync as readdirSync10 } from "fs";
6143
+ import { existsSync as existsSync28, readFileSync as readFileSync19, readdirSync as readdirSync11 } from "fs";
5940
6144
  import { join as join31 } from "path";
5941
6145
  function expectedPackages(dir) {
5942
6146
  const npmLock = join31(dir, "package-lock.json");
5943
6147
  if (existsSync28(npmLock)) {
5944
6148
  try {
5945
- const lock = JSON.parse(readFileSync18(npmLock, "utf8"));
6149
+ const lock = JSON.parse(readFileSync19(npmLock, "utf8"));
5946
6150
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
5947
6151
  } catch {
5948
6152
  return 0;
@@ -5951,7 +6155,7 @@ function expectedPackages(dir) {
5951
6155
  const pnpmLock = join31(dir, "pnpm-lock.yaml");
5952
6156
  if (existsSync28(pnpmLock)) {
5953
6157
  try {
5954
- const text = readFileSync18(pnpmLock, "utf8");
6158
+ const text = readFileSync19(pnpmLock, "utf8");
5955
6159
  const packages = text.slice(text.indexOf("\npackages:"));
5956
6160
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
5957
6161
  } catch {
@@ -5965,11 +6169,11 @@ function installedPackages(dir) {
5965
6169
  if (!existsSync28(root)) return 0;
5966
6170
  let total = 0;
5967
6171
  try {
5968
- for (const entry of readdirSync10(root, { withFileTypes: true })) {
6172
+ for (const entry of readdirSync11(root, { withFileTypes: true })) {
5969
6173
  if (!entry.isDirectory() || entry.name === ".bin") continue;
5970
6174
  if (entry.name.startsWith("@")) {
5971
6175
  try {
5972
- total += readdirSync10(join31(root, entry.name)).length;
6176
+ total += readdirSync11(join31(root, entry.name)).length;
5973
6177
  } catch {
5974
6178
  }
5975
6179
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -6051,7 +6255,7 @@ function createCli() {
6051
6255
  printNextSteps(result, installed);
6052
6256
  }
6053
6257
  );
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(
6258
+ 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
6259
  async (generator, name, second, options2) => {
6056
6260
  if (generator === "billing")
6057
6261
  return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The flare command-line tool: create, generate, migrate, and deploy Flare apps",
5
5
  "license": "MIT",
6
6
  "author": "Muke Johnbaptist",
@@ -41,7 +41,7 @@
41
41
  "jiti": "^2.7.0",
42
42
  "jsonc-parser": "^3.3.1",
43
43
  "picocolors": "^1.1.1",
44
- "@flaredev/core": "0.9.0"
44
+ "@flaredev/core": "0.10.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22"
@@ -96,8 +96,8 @@ export function DashboardSidebar({
96
96
  </SidebarMenu>
97
97
  </SidebarHeader>
98
98
 
99
- <SidebarContent>
100
- <SidebarGroup>
99
+ <SidebarContent className="gap-0">
100
+ <SidebarGroup className="px-2 py-1">
101
101
  <SidebarGroupContent>
102
102
  <SidebarMenu>
103
103
  <SidebarMenuItem>
@@ -113,8 +113,8 @@ export function DashboardSidebar({
113
113
  </SidebarGroup>
114
114
 
115
115
  {byGroup(resources).map(([heading, group]) => (
116
- <SidebarGroup key={heading}>
117
- <SidebarGroupLabel>{heading}</SidebarGroupLabel>
116
+ <SidebarGroup key={heading} className="px-2 py-1">
117
+ <SidebarGroupLabel className="mt-1 h-6">{heading}</SidebarGroupLabel>
118
118
  <SidebarGroupContent>
119
119
  <SidebarMenu>
120
120
  {group.map((resource) => {
@@ -149,8 +149,8 @@ export function DashboardSidebar({
149
149
  </SidebarGroup>
150
150
  ))}
151
151
 
152
- <SidebarGroup>
153
- <SidebarGroupLabel>Manage</SidebarGroupLabel>
152
+ <SidebarGroup className="px-2 py-1">
153
+ <SidebarGroupLabel className="mt-1 h-6">Manage</SidebarGroupLabel>
154
154
  <SidebarGroupContent>
155
155
  <SidebarMenu>
156
156
  {manage.map((link) => (
@@ -168,8 +168,8 @@ export function DashboardSidebar({
168
168
  </SidebarGroup>
169
169
 
170
170
  {links.length > 0 && (
171
- <SidebarGroup>
172
- <SidebarGroupLabel>Platform</SidebarGroupLabel>
171
+ <SidebarGroup className="px-2 py-1">
172
+ <SidebarGroupLabel className="mt-1 h-6">Platform</SidebarGroupLabel>
173
173
  <SidebarGroupContent>
174
174
  <SidebarMenu>
175
175
  {links.map((link) => {