@flaredev/cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.ts
2
- import pc23 from "picocolors";
2
+ import pc24 from "picocolors";
3
3
 
4
4
  // src/cli.ts
5
5
  import { cac } from "cac";
6
- import pc22 from "picocolors";
6
+ import pc23 from "picocolors";
7
7
  import * as prompts3 from "@clack/prompts";
8
8
  import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
9
9
 
@@ -139,6 +139,7 @@ function parseTheme(value) {
139
139
  }
140
140
 
141
141
  // src/versions.ts
142
+ var COMPATIBILITY_DATE = "2026-09-15";
142
143
  var APP_DEPENDENCIES = {
143
144
  "@better-auth/drizzle-adapter": "1.7.5",
144
145
  "@better-auth/passkey": "1.7.5",
@@ -280,7 +281,7 @@ function createApp(target, options2 = {}) {
280
281
  const authMethods = parseAuthMethods(options2.auth);
281
282
  const theme = parseTheme(options2.theme);
282
283
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
283
- const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
284
+ const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
284
285
  const files = copyTemplate(join2(templatesDir, "app"), dir, {
285
286
  APP_NAME: name,
286
287
  COMPAT_DATE: compatibilityDate,
@@ -379,6 +380,20 @@ function splitTopLevel(input) {
379
380
  parts.push(current);
380
381
  return parts.map((part) => part.trim()).filter(Boolean);
381
382
  }
383
+ var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
384
+ var SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
385
+ function parseSize(text, spec) {
386
+ const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i.exec(text.trim());
387
+ if (!match) {
388
+ throw new FieldGrammarError(`"${spec}": "${text}" isn't a size. Write it like "file:[image]:5mb".`);
389
+ }
390
+ const bytes = Math.round(Number(match[1]) * SIZE_UNITS[match[2].toLowerCase()]);
391
+ if (bytes < 1) throw new FieldGrammarError(`"${spec}": a size limit has to be at least one byte.`);
392
+ if (bytes > MAX_UPLOAD_BYTES) {
393
+ throw new FieldGrammarError(`"${spec}": ${text} is more than a Worker will carry in one request; the most is 100mb.`);
394
+ }
395
+ return bytes;
396
+ }
382
397
  function editDistance(a, b) {
383
398
  const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
384
399
  for (let j = 1; j <= b.length; j++) dp[0][j] = j;
@@ -439,11 +454,14 @@ function parseField(spec) {
439
454
  }
440
455
  if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
441
456
  field.options = options2;
442
- } else if (match = /^file(?::\[(.*)\])?$/.exec(type)) {
457
+ } else if (/^file\[/.test(type)) {
458
+ throw new FieldGrammarError(`"${spec}": file fields need a colon before the list, e.g. "${field.key}:file:[${type.slice(5, -1) || "image"}]".`);
459
+ } else if (match = /^file(?::\[(.*)\])?(?::([^:]*))?$/.exec(type)) {
443
460
  field.kind = "file";
444
461
  if (match[1] === void 0) {
445
462
  throw new FieldGrammarError(`"${spec}": file fields list accepted types, e.g. "file:[image,pdf]".`);
446
463
  }
464
+ if (match[2] !== void 0) field.maxBytes = parseSize(match[2], spec);
447
465
  const categories = Object.keys(FILE_CATEGORIES);
448
466
  const accept = list(match[1], "file type", spec).map((category) => category.toLowerCase());
449
467
  for (const category of accept) {
@@ -592,6 +610,7 @@ function options(field) {
592
610
  if (field.format) opts.format = field.format;
593
611
  if (field.widget) opts.widget = field.widget;
594
612
  if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
613
+ if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
595
614
  return opts;
596
615
  }
597
616
  function toField(field) {
@@ -1334,7 +1353,7 @@ async function loadResources(root) {
1334
1353
  const appRoot = resolve3(root);
1335
1354
  const dir = join3(appRoot, "resources");
1336
1355
  if (!existsSync4(dir)) return [];
1337
- const jiti = createJiti(join3(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1356
+ const jiti = createJiti(join3(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
1338
1357
  const loaded = [];
1339
1358
  for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
1340
1359
  const path = join3(dir, file);
@@ -1534,6 +1553,7 @@ function parseDeployArgs(args) {
1534
1553
  if (arg === "--skip-migrations") skipMigrations = true;
1535
1554
  else if (arg === "--skip-secrets") skipSecrets = true;
1536
1555
  else if (arg === "--skip-security") skipSecurity = true;
1556
+ else if (arg === "--yes" || arg === "-y") continue;
1537
1557
  else {
1538
1558
  forwarded.push(arg);
1539
1559
  if (arg === "--env" && args[i + 1]) env = args[i + 1];
@@ -3954,12 +3974,121 @@ Run it with ${pc17.bold(`flare seed ${name}`)}.`);
3954
3974
  return relative2;
3955
3975
  }
3956
3976
 
3957
- // src/commands/seed-resource.ts
3977
+ // src/commands/db-push.ts
3958
3978
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
3959
3979
  import { tmpdir as tmpdir2 } from "os";
3960
3980
  import { join as join22 } from "path";
3961
3981
  import pc18 from "picocolors";
3962
3982
 
3983
+ // src/terminal.ts
3984
+ var isTTY = () => Boolean(process.stdout.isTTY);
3985
+ function formatDuration(ms) {
3986
+ const seconds = Math.round(ms / 1e3);
3987
+ if (seconds < 60) return `${seconds}s`;
3988
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
3989
+ return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
3990
+ }
3991
+ var formatCount = (value) => value.toLocaleString();
3992
+ function formatRate(count, ms, unit = "rows") {
3993
+ if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
3994
+ return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
3995
+ }
3996
+ function progressLine(everyMs = 120) {
3997
+ if (!isTTY()) return { update: () => {
3998
+ }, done: () => {
3999
+ } };
4000
+ let last = 0;
4001
+ let width = 0;
4002
+ const write = (message) => {
4003
+ process.stdout.write(`\r${message.padEnd(width)}`);
4004
+ width = Math.max(width, message.length);
4005
+ };
4006
+ return {
4007
+ update(message) {
4008
+ const now = Date.now();
4009
+ if (now - last < everyMs) return;
4010
+ last = now;
4011
+ write(message);
4012
+ },
4013
+ done() {
4014
+ if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
4015
+ width = 0;
4016
+ }
4017
+ };
4018
+ }
4019
+
4020
+ // src/commands/db-push.ts
4021
+ var PAGE = 2e3;
4022
+ function pickDatabase(databases, wanted) {
4023
+ if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4024
+ if (wanted) {
4025
+ const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
4026
+ if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
4027
+ return match;
4028
+ }
4029
+ if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
4030
+ return databases[0];
4031
+ }
4032
+ async function dbPush(tables, options2 = {}) {
4033
+ const log2 = options2.log ?? ((message) => console.log(message));
4034
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4035
+ if (tables.length === 0) throw new Error("Name at least one table, e.g. flare db:push products categories --yes");
4036
+ const bad = tables.find((table) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(table));
4037
+ if (bad) throw new Error(`"${bad}" isn't a table name.`);
4038
+ if (!options2.yes) {
4039
+ throw new Error(`This writes ${tables.join(", ")} to the deployed database. Re-run with --yes to confirm.`);
4040
+ }
4041
+ const db = pickDatabase(readD1Databases(appRoot), options2.database);
4042
+ const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4043
+ const started = Date.now();
4044
+ const local = await openLocalD1(appRoot, options2.database);
4045
+ const dir = mkdtempSync2(join22(tmpdir2(), "flare-push-"));
4046
+ try {
4047
+ const statements = [];
4048
+ const counts = [];
4049
+ for (const table of tables) {
4050
+ if (options2.truncate) statements.push(`DELETE FROM "${table}";`);
4051
+ let offset = 0;
4052
+ let rows = 0;
4053
+ for (; ; ) {
4054
+ const page = await local.db.prepare(`SELECT * FROM "${table}" LIMIT ${PAGE} OFFSET ${offset}`).all();
4055
+ const results = page.results ?? [];
4056
+ if (results.length === 0) break;
4057
+ const columns = Object.keys(results[0]);
4058
+ for (const statement of insertStatements(table, columns, results)) statements.push(statement.sql);
4059
+ rows += results.length;
4060
+ if (results.length < PAGE) break;
4061
+ offset += PAGE;
4062
+ }
4063
+ counts.push({ table, rows });
4064
+ log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
4065
+ }
4066
+ const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
4067
+ if (total === 0) {
4068
+ log2("Nothing to push: those tables are empty locally.");
4069
+ return { tables: counts, ms: Date.now() - started };
4070
+ }
4071
+ const file = join22(dir, "push.sql");
4072
+ writeFileSync15(file, statements.join("\n"));
4073
+ const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4074
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4075
+ if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
4076
+ ${result.output.trim()}`);
4077
+ const ms = Date.now() - started;
4078
+ log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4079
+ return { tables: counts, ms };
4080
+ } finally {
4081
+ rmSync3(dir, { recursive: true, force: true });
4082
+ await local.dispose();
4083
+ }
4084
+ }
4085
+
4086
+ // src/commands/seed-resource.ts
4087
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4088
+ import { tmpdir as tmpdir3 } from "os";
4089
+ import { join as join23 } from "path";
4090
+ import pc19 from "picocolors";
4091
+
3963
4092
  // src/seed/rows.ts
3964
4093
  import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
3965
4094
  var has = (key, ...words) => words.some((word) => key.includes(word));
@@ -3981,7 +4110,9 @@ function stringValue(key, def, fake, resource, index) {
3981
4110
  return fake.slug();
3982
4111
  }
3983
4112
  const name = key.toLowerCase();
3984
- const organisation = has(resource.name.toLowerCase(), "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
4113
+ const resourceName2 = resource.name.toLowerCase();
4114
+ const organisation = has(resourceName2, "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
4115
+ const catalogue = has(resourceName2, "product", "item", "article", "sku", "listing", "merch", "book", "course", "plan", "service");
3985
4116
  let value;
3986
4117
  if (has(name, "email")) value = fake.email();
3987
4118
  else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
@@ -3996,7 +4127,7 @@ function stringValue(key, def, fake, resource, index) {
3996
4127
  else if (has(name, "colour", "color")) value = fake.color();
3997
4128
  else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
3998
4129
  else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
3999
- else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
4130
+ else if (has(name, "name")) value = organisation ? fake.company() : catalogue ? fake.product(digitalish(resource)) : fake.fullName();
4000
4131
  else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
4001
4132
  else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
4002
4133
  if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
@@ -4058,6 +4189,11 @@ function uniquify(value, row) {
4058
4189
  if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
4059
4190
  return `${value} ${row}`;
4060
4191
  }
4192
+ function digitalish(resource) {
4193
+ return Object.entries(resource.fields).some(
4194
+ ([key, def]) => def.kind === "enum" && /kind|type/.test(key) && def.options.some((option) => /digital|download|licence|license/i.test(option))
4195
+ );
4196
+ }
4061
4197
  function seedColumns(resource) {
4062
4198
  return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4063
4199
  }
@@ -4082,43 +4218,6 @@ function* seedRows(resource, count, options2 = {}) {
4082
4218
  }
4083
4219
  }
4084
4220
 
4085
- // src/terminal.ts
4086
- var isTTY = () => Boolean(process.stdout.isTTY);
4087
- function formatDuration(ms) {
4088
- const seconds = Math.round(ms / 1e3);
4089
- if (seconds < 60) return `${seconds}s`;
4090
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
4091
- return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
4092
- }
4093
- var formatCount = (value) => value.toLocaleString();
4094
- function formatRate(count, ms, unit = "rows") {
4095
- if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
4096
- return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
4097
- }
4098
- function progressLine(everyMs = 120) {
4099
- if (!isTTY()) return { update: () => {
4100
- }, done: () => {
4101
- } };
4102
- let last = 0;
4103
- let width = 0;
4104
- const write = (message) => {
4105
- process.stdout.write(`\r${message.padEnd(width)}`);
4106
- width = Math.max(width, message.length);
4107
- };
4108
- return {
4109
- update(message) {
4110
- const now = Date.now();
4111
- if (now - last < everyMs) return;
4112
- last = now;
4113
- write(message);
4114
- },
4115
- done() {
4116
- if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
4117
- width = 0;
4118
- }
4119
- };
4120
- }
4121
-
4122
4221
  // src/commands/seed-resource.ts
4123
4222
  async function countRows(db, table) {
4124
4223
  const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
@@ -4143,7 +4242,7 @@ function pickResource(resources, name) {
4143
4242
  }
4144
4243
  return match;
4145
4244
  }
4146
- function pickDatabase(databases, wanted) {
4245
+ function pickDatabase2(databases, wanted) {
4147
4246
  if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4148
4247
  if (wanted) {
4149
4248
  const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
@@ -4172,13 +4271,13 @@ async function seedResourceCommand(name, options2 = {}) {
4172
4271
  ...options2,
4173
4272
  onProgress: (written, total) => {
4174
4273
  const elapsed = Date.now() - started;
4175
- progress.update(` ${pc18.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc18.dim(formatRate(written, elapsed))}`);
4274
+ progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
4176
4275
  }
4177
4276
  });
4178
4277
  progress.done();
4179
4278
  const { rows, ms, where } = result;
4180
4279
  console.log(
4181
- `${pc18.green("\u2714")} Seeded ${pc18.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc18.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4280
+ `${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4182
4281
  );
4183
4282
  return result;
4184
4283
  } catch (error) {
@@ -4202,7 +4301,7 @@ async function seedResource(name, options2 = {}) {
4202
4301
  let startIndex = 0;
4203
4302
  const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
4204
4303
  if (options2.remote) {
4205
- const db = pickDatabase(readD1Databases(appRoot), options2.database);
4304
+ const db = pickDatabase2(readD1Databases(appRoot), options2.database);
4206
4305
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4207
4306
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4208
4307
  const query = async (sql2) => {
@@ -4221,8 +4320,8 @@ ${result.output.trim()}`);
4221
4320
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4222
4321
  startIndex = counted?.total ?? 0;
4223
4322
  }
4224
- const dir = mkdtempSync2(join22(tmpdir2(), "flare-seed-"));
4225
- const file = join22(dir, `${resource.table}.sql`);
4323
+ const dir = mkdtempSync3(join23(tmpdir3(), "flare-seed-"));
4324
+ const file = join23(dir, `${resource.table}.sql`);
4226
4325
  try {
4227
4326
  const statements = [];
4228
4327
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4232,13 +4331,13 @@ ${result.output.trim()}`);
4232
4331
  written += statement.rows;
4233
4332
  options2.onProgress?.(written, total);
4234
4333
  }
4235
- writeFileSync15(file, statements.join("\n"));
4236
- log2(`${pc18.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc18.bold(db.database_name)}`);
4334
+ writeFileSync16(file, statements.join("\n"));
4335
+ log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4237
4336
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4238
4337
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4239
4338
  ${result.output.trim()}`);
4240
4339
  } finally {
4241
- rmSync3(dir, { recursive: true, force: true });
4340
+ rmSync4(dir, { recursive: true, force: true });
4242
4341
  }
4243
4342
  return { resource, rows: total, ms: Date.now() - started, where };
4244
4343
  }
@@ -4265,15 +4364,15 @@ ${result.output.trim()}`);
4265
4364
 
4266
4365
  // src/commands/billing-sync.ts
4267
4366
  import { createRequire as createRequire2 } from "module";
4268
- import { existsSync as existsSync22, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4269
- import { tmpdir as tmpdir3 } from "os";
4270
- import { join as join23 } from "path";
4367
+ import { existsSync as existsSync22, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
4368
+ import { tmpdir as tmpdir4 } from "os";
4369
+ import { join as join24 } from "path";
4271
4370
  import { pathToFileURL as pathToFileURL2 } from "url";
4272
4371
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4273
- import pc19 from "picocolors";
4372
+ import pc20 from "picocolors";
4274
4373
  function stripeKeys(appRoot) {
4275
4374
  const map = {};
4276
- const path = join23(appRoot, ".dev.vars");
4375
+ const path = join24(appRoot, ".dev.vars");
4277
4376
  if (existsSync22(path)) {
4278
4377
  for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
4279
4378
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
@@ -4358,10 +4457,10 @@ async function syncPlans(options2 = {}) {
4358
4457
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4359
4458
  const db = readD1Databases(appRoot)[0];
4360
4459
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4361
- const appRequire = createRequire2(join23(appRoot, "package.json"));
4460
+ const appRequire = createRequire2(join24(appRoot, "package.json"));
4362
4461
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4363
4462
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4364
- const appName = JSON.parse(readFileSync13(join23(appRoot, "package.json"), "utf8")).name ?? "app";
4463
+ const appName = JSON.parse(readFileSync13(join24(appRoot, "package.json"), "utf8")).name ?? "app";
4365
4464
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4366
4465
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4367
4466
  if (products.length === 0) {
@@ -4382,41 +4481,41 @@ async function syncPlans(options2 = {}) {
4382
4481
  ${existingResult.output.trim()}`);
4383
4482
  }
4384
4483
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4385
- const dir = mkdtempSync3(join23(tmpdir3(), "flare-sync-"));
4484
+ const dir = mkdtempSync4(join24(tmpdir4(), "flare-sync-"));
4386
4485
  try {
4387
- const file = join23(dir, "sync-plans.sql");
4388
- writeFileSync16(file, renderSyncSql(rows));
4486
+ const file = join24(dir, "sync-plans.sql");
4487
+ writeFileSync17(file, renderSyncSql(rows));
4389
4488
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4390
4489
  if (result.code !== 0) throw new Error(`Could not write plans:
4391
4490
  ${result.output.trim()}`);
4392
4491
  } finally {
4393
- rmSync4(dir, { recursive: true, force: true });
4492
+ rmSync5(dir, { recursive: true, force: true });
4394
4493
  }
4395
4494
  let created = 0;
4396
4495
  for (const row of rows) {
4397
4496
  const isNew = !known.has(row.stripePriceId);
4398
4497
  if (isNew) created++;
4399
4498
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
4400
- log2(`${(isNew ? pc19.green("create") : pc19.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc19.dim(price)}`);
4499
+ log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
4401
4500
  }
4402
- for (const name of skipped) log2(pc19.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4501
+ for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4403
4502
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
4404
- if (configuration) log2(`${pc19.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4405
- log2(pc19.green(`
4503
+ if (configuration) log2(`${pc20.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4504
+ log2(pc20.green(`
4406
4505
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
4407
4506
  return { created, updated: rows.length - created, skipped };
4408
4507
  }
4409
4508
 
4410
4509
  // src/commands/sync.ts
4411
4510
  import { existsSync as existsSync23 } from "fs";
4412
- import { join as join24 } from "path";
4413
- import pc20 from "picocolors";
4511
+ import { join as join25 } from "path";
4512
+ import pc21 from "picocolors";
4414
4513
  async function syncTypes(options2 = {}) {
4415
4514
  const log2 = options2.log ?? ((message) => console.log(message));
4416
4515
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4417
4516
  const all = await loadResources(appRoot);
4418
4517
  const policies = await loadPolicies(appRoot);
4419
- const tracksPolicies = policies.length > 0 || existsSync23(join24(appRoot, "policies/index.ts"));
4518
+ const tracksPolicies = policies.length > 0 || existsSync23(join25(appRoot, "policies/index.ts"));
4420
4519
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
4421
4520
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4422
4521
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -4428,42 +4527,42 @@ async function syncTypes(options2 = {}) {
4428
4527
  if (drift.length) {
4429
4528
  log2(
4430
4529
  `
4431
- ${pc20.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4530
+ ${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4432
4531
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
4433
4532
  );
4434
4533
  }
4435
4534
  if (unmarked.length) {
4436
4535
  log2(`
4437
- ${pc20.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4536
+ ${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4438
4537
  }
4439
4538
  if (orphans.length) {
4440
4539
  const resources = [...new Set(orphans.map((o) => o.resource))];
4441
4540
  log2(`
4442
- ${pc20.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4541
+ ${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4443
4542
  for (const orphan of orphans) log2(` ${orphan.path}`);
4444
4543
  log2(
4445
- `If the resource was deleted, remove them with ${pc20.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4544
+ `If the resource was deleted, remove them with ${pc21.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4446
4545
  );
4447
4546
  }
4448
4547
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
4449
4548
  if (tablesChanged && !options2.check) {
4450
4549
  log2(`
4451
- ${pc20.cyan("Tables changed.")} Create a migration with ${pc20.bold("flare gen migration <name> --from-schema")}, then ${pc20.bold("flare migrate")}.`);
4550
+ ${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
4452
4551
  }
4453
4552
  const problems = drift.length + unmarked.length + orphans.length;
4454
4553
  if (options2.check) {
4455
4554
  const stale = changed.length;
4456
- if (stale === 0 && problems === 0) log2(pc20.green("Generated files are in sync with the resource descriptors."));
4555
+ if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
4457
4556
  else log2(`
4458
- ${pc20.red("Out of sync:")} ${stale} file(s) would change. Run ${pc20.bold("flare sync-types")}.`);
4557
+ ${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
4459
4558
  return stale + problems > 0 ? 1 : 0;
4460
4559
  }
4461
- if (changed.length === 0 && problems === 0) log2(pc20.green("Everything is in sync."));
4560
+ if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
4462
4561
  return problems > 0 ? 1 : 0;
4463
4562
  }
4464
4563
 
4465
4564
  // src/commands/user-role.ts
4466
- import pc21 from "picocolors";
4565
+ import pc22 from "picocolors";
4467
4566
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
4468
4567
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
4469
4568
  function userRoleSql(email, role) {
@@ -4477,8 +4576,8 @@ async function setUserRole(email, role, options2 = {}) {
4477
4576
  const sql2 = userRoleSql(email, role);
4478
4577
  const roles = await listRoles(options2);
4479
4578
  if (roles && !roles.includes(role)) {
4480
- log2(pc21.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4481
- log2(pc21.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4579
+ log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4580
+ log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4482
4581
  return 1;
4483
4582
  }
4484
4583
  const db = readD1Databases(appRoot)[0];
@@ -4492,19 +4591,19 @@ ${result.output.trim()}`);
4492
4591
  const [first] = extractJson(result.output);
4493
4592
  const updated = first?.results ?? [];
4494
4593
  if (updated.length === 0) {
4495
- log2(pc21.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4594
+ log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4496
4595
  return 1;
4497
4596
  }
4498
- log2(pc21.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4597
+ log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4499
4598
  return 0;
4500
4599
  }
4501
4600
 
4502
4601
  // src/utils/install.ts
4503
4602
  import { spawn as spawn5 } from "child_process";
4504
4603
  import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
4505
- import { join as join25 } from "path";
4604
+ import { join as join26 } from "path";
4506
4605
  function expectedPackages(dir) {
4507
- const npmLock = join25(dir, "package-lock.json");
4606
+ const npmLock = join26(dir, "package-lock.json");
4508
4607
  if (existsSync24(npmLock)) {
4509
4608
  try {
4510
4609
  const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
@@ -4513,7 +4612,7 @@ function expectedPackages(dir) {
4513
4612
  return 0;
4514
4613
  }
4515
4614
  }
4516
- const pnpmLock = join25(dir, "pnpm-lock.yaml");
4615
+ const pnpmLock = join26(dir, "pnpm-lock.yaml");
4517
4616
  if (existsSync24(pnpmLock)) {
4518
4617
  try {
4519
4618
  const text = readFileSync14(pnpmLock, "utf8");
@@ -4526,7 +4625,7 @@ function expectedPackages(dir) {
4526
4625
  return 0;
4527
4626
  }
4528
4627
  function installedPackages(dir) {
4529
- const root = join25(dir, "node_modules");
4628
+ const root = join26(dir, "node_modules");
4530
4629
  if (!existsSync24(root)) return 0;
4531
4630
  let total = 0;
4532
4631
  try {
@@ -4534,7 +4633,7 @@ function installedPackages(dir) {
4534
4633
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4535
4634
  if (entry.name.startsWith("@")) {
4536
4635
  try {
4537
- total += readdirSync9(join25(root, entry.name)).length;
4636
+ total += readdirSync9(join26(root, entry.name)).length;
4538
4637
  } catch {
4539
4638
  }
4540
4639
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -4594,7 +4693,7 @@ function createCli() {
4594
4693
  });
4595
4694
  installed = outcome.code === 0;
4596
4695
  if (installed) {
4597
- spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc22.dim(`(${formatDuration(outcome.ms)})`)}`);
4696
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
4598
4697
  } else {
4599
4698
  spin.error(`${result.packageManager} install failed`);
4600
4699
  process.stderr.write(outcome.output);
@@ -4643,6 +4742,9 @@ function createCli() {
4643
4742
  cli.command("seed:resource <resource> [count]", "Fill a resource's table with sample rows built from its descriptor").option("--count <rows>", "How many rows: 1000, 25k, 1m (default: 25)").option("--remote", "Seed the deployed database instead of the local one (needs --yes)").option("--truncate", "Delete the table's rows first").option("--seed <number>", "Same number, same rows").option("--env <name>", "Wrangler environment").option("--database <binding>", "D1 binding, when the app has several").option("-y, --yes", "Confirm writing to the remote database").example("flare seed:resource Contact --count 1000").example("flare seed:resource Contact 1m # a million rows").example("flare seed:resource Contact 5k --remote --yes").action(async (resource, count, options2) => {
4644
4743
  await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
4645
4744
  });
4745
+ cli.command("db:push [...tables]", "Copy rows from the local database to the deployed one (seeded catalogues, reference data)").option("--truncate", "Delete the remote rows of those tables first").option("--database <binding>", "D1 binding, when the app has several").option("--env <name>", "Wrangler environment").option("-y, --yes", "Confirm writing to the deployed database").example("flare db:push products categories --yes").example("flare db:push plans --truncate --yes").action(async (tables, options2) => {
4746
+ await dbPush(tables, options2);
4747
+ });
4646
4748
  cli.command("seed:make <name>", "Create seeds/<name>.seed.ts (with example rows for a matching resource)").option("--resource <name>", "Resource to write example rows for").action(async (name, options2) => {
4647
4749
  await makeSeed(name, options2);
4648
4750
  });
@@ -4688,6 +4790,6 @@ try {
4688
4790
  }
4689
4791
  }
4690
4792
  } catch (error) {
4691
- console.error(pc23.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4793
+ console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4692
4794
  process.exitCode = 1;
4693
4795
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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.4.0"
44
+ "@flaredev/core": "0.4.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22"