@flaredev/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/bin/flare.js +9 -1
  2. package/dist/index.js +991 -344
  3. package/package.json +2 -2
  4. package/templates/app/app/admin/page.tsx +9 -61
  5. package/templates/app/app/dashboard/account/layout.tsx +30 -0
  6. package/templates/app/app/dashboard/account/page.tsx +11 -31
  7. package/templates/app/app/dashboard/account/password/page.tsx +22 -0
  8. package/templates/app/app/dashboard/account/security/page.tsx +34 -0
  9. package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
  10. package/templates/app/app/dashboard/actions.ts +246 -0
  11. package/templates/app/app/dashboard/import-actions.ts +91 -0
  12. package/templates/app/app/dashboard/layout.tsx +66 -39
  13. package/templates/app/app/dashboard/observability/page.tsx +175 -0
  14. package/templates/app/app/dashboard/page.tsx +108 -27
  15. package/templates/app/app/verify-email/page.tsx +33 -0
  16. package/templates/app/components/account/account-nav.tsx +39 -0
  17. package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -525
  18. package/templates/app/components/auth/otp-input.tsx +95 -0
  19. package/templates/app/components/auth/password-field.tsx +116 -0
  20. package/templates/app/components/auth/password-forms.tsx +76 -79
  21. package/templates/app/components/auth/sign-in-flow.tsx +306 -315
  22. package/templates/app/components/auth/sign-up-form.tsx +93 -101
  23. package/templates/app/components/auth/two-factor-form.tsx +139 -124
  24. package/templates/app/components/auth/use-resend.ts +44 -0
  25. package/templates/app/components/auth/verify-email-form.tsx +135 -0
  26. package/templates/app/components/dashboard/column-menu.tsx +66 -0
  27. package/templates/app/components/dashboard/dashboard-header.tsx +115 -0
  28. package/templates/app/components/dashboard/dashboard-sidebar.tsx +228 -0
  29. package/templates/app/components/dashboard/dropzone.tsx +92 -0
  30. package/templates/app/components/dashboard/editable.ts +12 -0
  31. package/templates/app/components/dashboard/export-button.tsx +41 -0
  32. package/templates/app/components/{admin → dashboard}/fields/file-field.tsx +22 -18
  33. package/templates/app/components/dashboard/import-dialog.tsx +540 -0
  34. package/templates/app/components/dashboard/inline-cell.tsx +165 -0
  35. package/templates/app/components/dashboard/local-time.tsx +77 -0
  36. package/templates/app/components/dashboard/page-header.tsx +57 -0
  37. package/templates/app/components/{admin → dashboard}/query.ts +2 -0
  38. package/templates/app/components/dashboard/record-actions.tsx +113 -0
  39. package/templates/app/components/dashboard/record-detail.tsx +200 -0
  40. package/templates/app/components/dashboard/resource-chart.tsx +91 -0
  41. package/templates/app/components/dashboard/resource-form-dialog.tsx +80 -0
  42. package/templates/app/components/{admin → dashboard}/resource-form-page.tsx +61 -55
  43. package/templates/app/components/{admin → dashboard}/resource-form.tsx +165 -139
  44. package/templates/app/components/dashboard/resource-stats.tsx +48 -0
  45. package/templates/app/components/{admin → dashboard}/resource-table.tsx +152 -28
  46. package/templates/app/components/{admin → dashboard}/row-actions.tsx +56 -11
  47. package/templates/app/components/dashboard/save-view-button.tsx +86 -0
  48. package/templates/app/components/dashboard/stat-card.tsx +69 -0
  49. package/templates/app/components/dashboard/table-selection.tsx +185 -0
  50. package/templates/app/components/dashboard/traffic-chart.tsx +32 -0
  51. package/templates/app/components/dashboard/user-menu.tsx +91 -0
  52. package/templates/app/components/ui/sonner.tsx +33 -13
  53. package/templates/app/db/flare-schema.ts +55 -1
  54. package/templates/app/lib/analytics.ts +121 -0
  55. package/templates/app/lib/api.ts +12 -0
  56. package/templates/app/lib/audit.ts +80 -0
  57. package/templates/app/lib/auth-config.ts +38 -33
  58. package/templates/app/lib/auth.ts +34 -0
  59. package/templates/app/lib/cache.ts +83 -83
  60. package/templates/app/lib/csv.ts +24 -0
  61. package/templates/app/lib/dashboard-nav.ts +15 -0
  62. package/templates/app/lib/dashboard.ts +165 -0
  63. package/templates/app/lib/password-rules.ts +159 -0
  64. package/templates/app/lib/site.ts +56 -47
  65. package/templates/app/lib/views.ts +29 -0
  66. package/templates/app/migrations/0000_init.sql +25 -2
  67. package/templates/app/migrations/meta/0000_snapshot.json +159 -1
  68. package/templates/app/migrations/meta/_journal.json +1 -1
  69. package/templates/app/vite.config.ts +41 -31
  70. package/templates/app/worker-configuration.d.ts +15764 -0
  71. package/templates/locks/package-lock.json +9189 -0
  72. package/templates/locks/pnpm-lock.yaml +6347 -0
  73. package/templates/locks/yarn.lock +3548 -0
  74. package/templates/app/app/admin/actions.ts +0 -113
  75. package/templates/app/app/admin/layout.tsx +0 -32
  76. package/templates/app/components/admin/admin-header.tsx +0 -78
  77. package/templates/app/components/admin/resource-nav.tsx +0 -120
  78. package/templates/app/components/dashboard/dashboard-nav.tsx +0 -35
  79. package/templates/app/lib/admin-nav.ts +0 -15
  80. package/templates/app/lib/admin.ts +0 -103
  81. /package/templates/app/components/{admin → dashboard}/fields/country-field.tsx +0 -0
  82. /package/templates/app/components/{admin → dashboard}/fields/date-field.tsx +0 -0
  83. /package/templates/app/components/{admin → dashboard}/fields/field-widget.tsx +0 -0
  84. /package/templates/app/components/{admin → dashboard}/fields/multi-select-field.tsx +0 -0
  85. /package/templates/app/components/{admin → dashboard}/fields/phone-field.tsx +0 -0
  86. /package/templates/app/components/{admin → dashboard}/fields/relation-field.tsx +0 -0
  87. /package/templates/app/components/{admin → dashboard}/resource-icon.tsx +0 -0
  88. /package/templates/app/components/{admin → dashboard}/resource-table-toolbar.tsx +0 -0
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  // src/index.ts
2
- import pc19 from "picocolors";
2
+ import pc23 from "picocolors";
3
3
 
4
4
  // src/cli.ts
5
5
  import { cac } from "cac";
6
- import { FLARE_VERSION as FLARE_VERSION2 } from "@flaredev/core";
6
+ import pc22 from "picocolors";
7
+ import * as prompts3 from "@clack/prompts";
8
+ import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
7
9
 
8
10
  // src/commands/create.ts
9
11
  import { randomBytes } from "crypto";
10
- import { appendFileSync, existsSync as existsSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
12
+ import { appendFileSync, copyFileSync, existsSync as existsSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
11
13
  import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
14
+ import * as prompts from "@clack/prompts";
12
15
  import pc from "picocolors";
13
16
  import { FLARE_VERSION } from "@flaredev/core";
14
17
 
@@ -86,6 +89,11 @@ export const authConfig = {
86
89
  social: ${JSON.stringify(providers).replace(/","/g, '", "')} as SocialProvider[],
87
90
  /** Refuse password sign-in until the email address is verified. */
88
91
  requireEmailVerification: false,
92
+ /**
93
+ * Check new passwords against Have I Been Pwned's breach list. Only the first five
94
+ * characters of the password's hash are sent, and an outage lets the password through.
95
+ */
96
+ checkBreachedPasswords: true,
89
97
  };
90
98
 
91
99
  export type SocialProvider = "google" | "github" | "apple" | "microsoft";
@@ -185,16 +193,19 @@ import { fileURLToPath } from "url";
185
193
  var RENAMES = { _gitignore: ".gitignore" };
186
194
  function copyTemplate(from, to, tokens) {
187
195
  mkdirSync(to, { recursive: true });
196
+ let files = 0;
188
197
  for (const entry of readdirSync(from)) {
189
198
  const src = join(from, entry);
190
199
  const dest = join(to, RENAMES[entry] ?? entry);
191
200
  if (statSync(src).isDirectory()) {
192
- copyTemplate(src, dest, tokens);
201
+ files += copyTemplate(src, dest, tokens);
193
202
  continue;
194
203
  }
195
204
  const content = readFileSync(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
196
205
  writeFileSync(dest, content);
206
+ files++;
197
207
  }
208
+ return files;
198
209
  }
199
210
  function writeJson(path, value) {
200
211
  mkdirSync(dirname(path), { recursive: true });
@@ -234,11 +245,6 @@ function childEnv(env = process.env) {
234
245
  }
235
246
  return next;
236
247
  }
237
- function runQuiet(command, args, cwd) {
238
- const env = childEnv();
239
- const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, env, shell: true, encoding: "utf8" }) : spawnSync(command, args, { cwd, env, encoding: "utf8" });
240
- return { code: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
241
- }
242
248
  function installArgs(pm) {
243
249
  if (pm === "npm") return ["install", "--no-audit", "--no-fund", "--loglevel=error"];
244
250
  return ["install"];
@@ -275,7 +281,7 @@ function createApp(target, options2 = {}) {
275
281
  const theme = parseTheme(options2.theme);
276
282
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
277
283
  const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
278
- copyTemplate(join2(templatesDir, "app"), dir, {
284
+ const files = copyTemplate(join2(templatesDir, "app"), dir, {
279
285
  APP_NAME: name,
280
286
  COMPAT_DATE: compatibilityDate,
281
287
  PM: packageManager,
@@ -310,42 +316,34 @@ ${devVarsEntries(authProviders)}`
310
316
  if (packageManager === "pnpm" && !inWorkspace) {
311
317
  writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
312
318
  }
313
- const log = options2.log ?? ((message) => console.log(message));
314
- if (options2.install !== false) {
315
- log(`${pc.green("\u2714")} Wrote the app to ${relative(process.cwd(), dir) || "."}`);
316
- log(`${pc.cyan("\u25CF")} Installing dependencies with ${packageManager}. The first install downloads the Workers runtime and toolchain, so it can take a few minutes.
317
- `);
318
- const started = Date.now();
319
- const code = run(packageManager, installArgs(packageManager), dir);
320
- if (code !== 0) throw new Error(`${packageManager} install failed (exit code ${code}).`);
321
- log(`${pc.green("\u2714")} Installed dependencies ${pc.dim(`(${formatDuration(Date.now() - started)})`)}`);
322
- const typegen = runQuiet(packageManager, ["run", "cf-typegen"], dir);
323
- if (typegen.code !== 0) {
324
- process.stderr.write(typegen.output);
325
- throw new Error(`wrangler types failed (exit code ${typegen.code}).`);
326
- }
327
- log(`${pc.green("\u2714")} Generated types for the Cloudflare bindings`);
328
- }
319
+ const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
320
+ if (pinned) copyLockfile(packageManager, dir);
321
+ const log2 = options2.log ?? ((message) => console.log(message));
322
+ log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
329
323
  return { dir, name, packageManager, inWorkspace };
330
324
  }
331
- function formatDuration(ms) {
332
- const seconds = Math.round(ms / 1e3);
333
- return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
325
+ function copyLockfile(packageManager, dir) {
326
+ const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
327
+ if (!file) return false;
328
+ const source = join2(templatesDir, "locks", file);
329
+ if (!existsSync2(source)) return false;
330
+ copyFileSync(source, join2(dir, file));
331
+ return true;
334
332
  }
335
333
  function printNextSteps(result, installed) {
336
334
  const rel = relative(process.cwd(), result.dir) || ".";
337
335
  const pm = result.packageManager;
338
- const step = (command, note) => ` ${command.padEnd(30)}${pc.dim(note)}`;
339
- console.log(`
340
- ${pc.green("\u2714")} Created ${pc.bold(result.name)}
341
- `);
342
- console.log("Next steps:");
343
- console.log(` cd ${rel}`);
344
- if (!installed) console.log(` ${pm} install`);
345
- console.log(step(`${pm} run dev`, "start it at http://localhost:3000"));
346
- console.log(step("npx flare gen resource ...", "add your first resource"));
347
- console.log(`
348
- Quickstart: ${pc.cyan("https://flare-docs.codetotech.com/start/quickstart/")}`);
336
+ const commands = [
337
+ [`cd ${rel}`, "your new app"],
338
+ ...installed ? [] : [[`${pm} install`, "dependencies first"]],
339
+ [`${pm} run dev`, "http://localhost:3000"],
340
+ ["npx flare gen resource Contact", "a table, API and admin screens"],
341
+ ["npx flare seed:resource Contact 1000", "fill it with sample rows"]
342
+ ];
343
+ const width = Math.max(...commands.map(([command]) => command.length)) + 2;
344
+ const steps = commands.map(([command, note2]) => `${pc.cyan(command.padEnd(width))}${pc.dim(note2)}`);
345
+ prompts.note(steps.join("\n"), "Next");
346
+ prompts.outro(`${pc.bold(result.name)} is ready ${pc.dim("\xB7")} ${pc.cyan("https://flare-docs.codetotech.com/start/quickstart/")}`);
349
347
  }
350
348
 
351
349
  // src/commands/gen-billing.ts
@@ -640,15 +638,21 @@ function renderFieldsBlock(fields) {
640
638
  return fields.map((f) => `${renderField(f)}
641
639
  `).join("");
642
640
  }
643
- function renderDescriptor(name, fields) {
641
+ function renderDescriptor(name, fields, options2 = {}) {
644
642
  defineResource({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
645
643
  const block = renderFieldsBlock(fields);
644
+ const heading = [
645
+ options2.icon ? ` icon: ${literal(options2.icon)},
646
+ ` : "",
647
+ options2.group ? ` group: ${literal(options2.group)},
648
+ ` : ""
649
+ ].join("");
646
650
  return joinMarkers({
647
651
  before: `import { defineResource, field } from "@flaredev/core";
648
652
 
649
653
  export default defineResource({
650
654
  name: ${literal(name)},
651
- fields: {
655
+ ${heading} fields: {
652
656
  `,
653
657
  block,
654
658
  after: ` },
@@ -1394,12 +1398,12 @@ function onPath() {
1394
1398
  const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1395
1399
  return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
1396
1400
  }
1397
- async function download(log) {
1401
+ async function download(log2) {
1398
1402
  const asset = releaseAsset();
1399
1403
  const dir = cacheDir();
1400
1404
  mkdirSync3(dir, { recursive: true });
1401
1405
  const target = join4(dir, binName);
1402
- log(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1406
+ log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1403
1407
  const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1404
1408
  if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1405
1409
  const bytes = Buffer.from(await response.arrayBuffer());
@@ -1416,12 +1420,12 @@ async function download(log) {
1416
1420
  if (process.platform !== "win32") chmodSync(target, 493);
1417
1421
  return target;
1418
1422
  }
1419
- async function ensureCloudflared(log = console.log) {
1423
+ async function ensureCloudflared(log2 = console.log) {
1420
1424
  const found = onPath();
1421
1425
  if (found) return found;
1422
1426
  const cached = join4(cacheDir(), binName);
1423
1427
  if (existsSync5(cached)) return cached;
1424
- return download(log);
1428
+ return download(log2);
1425
1429
  }
1426
1430
  async function openTunnel(localUrl, options2 = {}) {
1427
1431
  const bin = await ensureCloudflared(options2.log);
@@ -1575,9 +1579,9 @@ async function runDeploy(args, context3) {
1575
1579
  if (plan.passthroughOnly) return context3.deploy(plan.forwarded);
1576
1580
  const { appRoot, wranglerBin } = context3;
1577
1581
  const shellEnv = context3.env ?? process.env;
1578
- const log = context3.log ?? ((message) => console.log(message));
1582
+ const log2 = context3.log ?? ((message) => console.log(message));
1579
1583
  const envArgs = plan.env ? ["--env", plan.env] : [];
1580
- const step = (message) => log(`
1584
+ const step = (message) => log2(`
1581
1585
  ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1582
1586
  const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1583
1587
  const migrateAfterDeploy = [];
@@ -1609,7 +1613,7 @@ ${info.output.trim()}`);
1609
1613
  const apiToken = shellEnv.FLARE_SECURITY_API_TOKEN;
1610
1614
  if (security && zoneId && apiToken) {
1611
1615
  step("Provisioning zone security rules");
1612
- await provisionZoneSecurity(appRoot, security, { zoneId, apiToken }, { log, fetch: context3.fetch });
1616
+ await provisionZoneSecurity(appRoot, security, { zoneId, apiToken }, { log: log2, fetch: context3.fetch });
1613
1617
  } else if (security) {
1614
1618
  step("Zone security rules skipped (Worker-layer protection is on). To add them, deploy with FLARE_SECURITY_ZONE_ID and FLARE_SECURITY_API_TOKEN set.");
1615
1619
  }
@@ -1642,7 +1646,7 @@ ${put.output.trim()}`);
1642
1646
  const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1643
1647
  if (optional.length > 0) {
1644
1648
  step("Optional secrets not set in production (features using them stay off):");
1645
- for (const name of optional) log(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
1649
+ for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
1646
1650
  }
1647
1651
  }
1648
1652
  return 0;
@@ -1849,7 +1853,7 @@ function previousSnapshot(dir, migrationFile) {
1849
1853
  }
1850
1854
 
1851
1855
  // src/generator/plan.ts
1852
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1856
+ import { copyFileSync as copyFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1853
1857
  import { dirname as dirname5, join as join10 } from "path";
1854
1858
  import pc5 from "picocolors";
1855
1859
 
@@ -1962,13 +1966,24 @@ function renderTableModule(entry, all) {
1962
1966
  const name = tableExport(resource);
1963
1967
  const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
1964
1968
  const extras = [];
1969
+ const indexed = /* @__PURE__ */ new Set();
1970
+ const addIndex = (key, column) => {
1971
+ if (indexed.has(key)) return;
1972
+ indexed.add(key);
1973
+ extras.push(` index(${q(`${resource.table}_${columnName(column)}_idx`)}).on(table.${key}),`);
1974
+ };
1965
1975
  for (const [key, def] of fields) {
1966
1976
  if (def.kind === "enum") {
1967
1977
  const options2 = def.options.map(sqlString).join(", ");
1968
1978
  extras.push(` check(${q(`${resource.table}_${columnName(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
1979
+ if (def.filterable !== false) addIndex(key, key);
1969
1980
  }
1970
- if (def.kind === "belongsTo") extras.push(` index(${q(`${resource.table}_${columnName(key)}_idx`)}).on(table.${key}),`);
1981
+ if (def.kind === "belongsTo") addIndex(key, key);
1971
1982
  }
1983
+ const sortKey = resource.defaultSort.field;
1984
+ if (sortKey === "createdAt" || sortKey === "updatedAt") addIndex(sortKey, sortKey === "createdAt" ? "created_at" : "updated_at");
1985
+ else if (fields.some(([key]) => key === sortKey)) addIndex(sortKey, sortKey);
1986
+ addIndex("createdAt", "created_at");
1972
1987
  const targets = [...new Set(fields.flatMap(([, def]) => def.kind === "belongsTo" ? [resources.get(def.target)] : []))].filter((target) => target.name !== resource.name).sort((a, b) => a.table.localeCompare(b.table));
1973
1988
  const coreImports = ["integer", "sqliteTable", "text"];
1974
1989
  if (fields.some(([, def]) => def.kind === "float")) coreImports.push("real");
@@ -2044,7 +2059,7 @@ var handlerImports = (entry) => [
2044
2059
  `import { createResourceHandlers } from "@flaredev/core/server";`,
2045
2060
  `import { getDb } from "@/db";`,
2046
2061
  `import { ${tableExport(entry.resource)} } from "@/db/schema";`,
2047
- `import { authorize } from "@/lib/api";`,
2062
+ `import { authorize, currentUser } from "@/lib/api";`,
2048
2063
  `import { revalidateResource } from "@/lib/cache";`,
2049
2064
  `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`,
2050
2065
  "",
@@ -2053,6 +2068,7 @@ var handlerImports = (entry) => [
2053
2068
  ` table: ${tableExport(entry.resource)},`,
2054
2069
  ` getDb,`,
2055
2070
  ` authorize,`,
2071
+ ` currentUser,`,
2056
2072
  ` onChange: revalidateResource,`,
2057
2073
  `});`,
2058
2074
  ""
@@ -2078,24 +2094,45 @@ var adminImports = (entry) => [
2078
2094
  function renderAdminListPage(entry) {
2079
2095
  const local = resourceLocal(entry.stem);
2080
2096
  return [
2081
- `import { ResourceTable } from "@/components/admin/resource-table";`,
2082
- `import type { SearchParams } from "@/components/admin/query";`,
2097
+ `import { PageHeader } from "@/components/dashboard/page-header";`,
2098
+ `import { ResourceChart } from "@/components/dashboard/resource-chart";`,
2099
+ `import { ResourceStats } from "@/components/dashboard/resource-stats";`,
2100
+ `import { ResourceTable } from "@/components/dashboard/resource-table";`,
2101
+ `import type { SearchParams } from "@/components/dashboard/query";`,
2083
2102
  ...adminImports(entry),
2084
2103
  "",
2104
+ `export const metadata = { title: ${local}.pluralLabel };`,
2105
+ "",
2085
2106
  `export default async function ${entry.resource.name}ListPage({ searchParams }: { searchParams: Promise<SearchParams> }) {`,
2086
2107
  " return (",
2087
- ` <div className="flex flex-col gap-6">`,
2088
- ` <h1 className="text-2xl font-semibold">{${local}.pluralLabel}</h1>`,
2108
+ " <>",
2109
+ " <PageHeader",
2110
+ ` title={${local}.pluralLabel}`,
2111
+ ` crumbs={[{ label: "Dashboard", href: "/dashboard" }, { label: ${local}.pluralLabel }]}`,
2112
+ " />",
2113
+ ` <ResourceStats resource={${local}} />`,
2114
+ ` <ResourceChart resource={${local}} />`,
2089
2115
  ` <ResourceTable resource={${local}} searchParams={await searchParams} />`,
2090
- " </div>",
2116
+ " </>",
2091
2117
  " );",
2092
2118
  "}",
2093
2119
  ""
2094
2120
  ].join("\n");
2095
2121
  }
2122
+ function renderAdminDetailPage(entry) {
2123
+ return [
2124
+ `import { RecordDetail } from "@/components/dashboard/record-detail";`,
2125
+ ...adminImports(entry),
2126
+ "",
2127
+ `export default async function ${entry.resource.name}DetailPage({ params }: { params: Promise<{ id: string }> }) {`,
2128
+ ` return <RecordDetail resource={${resourceLocal(entry.stem)}} id={(await params).id} />;`,
2129
+ "}",
2130
+ ""
2131
+ ].join("\n");
2132
+ }
2096
2133
  function renderAdminNewPage(entry) {
2097
2134
  return [
2098
- `import { ResourceFormPage } from "@/components/admin/resource-form-page";`,
2135
+ `import { ResourceFormPage } from "@/components/dashboard/resource-form-page";`,
2099
2136
  ...adminImports(entry),
2100
2137
  "",
2101
2138
  `export default function New${entry.resource.name}Page() {`,
@@ -2106,7 +2143,7 @@ function renderAdminNewPage(entry) {
2106
2143
  }
2107
2144
  function renderAdminEditPage(entry) {
2108
2145
  return [
2109
- `import { ResourceFormPage } from "@/components/admin/resource-form-page";`,
2146
+ `import { ResourceFormPage } from "@/components/dashboard/resource-form-page";`,
2110
2147
  ...adminImports(entry),
2111
2148
  "",
2112
2149
  `export default async function Edit${entry.resource.name}Page({ params }: { params: Promise<{ id: string }> }) {`,
@@ -2174,9 +2211,10 @@ function resourceFiles(entry, all) {
2174
2211
  { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry) },
2175
2212
  { path: `resources/${stem}.client.ts`, content: renderClient(entry) },
2176
2213
  { path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
2177
- { path: `app/admin/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2178
- { path: `app/admin/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2179
- { path: `app/admin/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
2214
+ { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2215
+ { path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
2216
+ { path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2217
+ { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
2180
2218
  ];
2181
2219
  }
2182
2220
 
@@ -2236,8 +2274,8 @@ function findOrphans(appRoot, all) {
2236
2274
  for (const dir of list2("app/api", () => true)) {
2237
2275
  for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
2238
2276
  }
2239
- for (const dir of list2("app/admin", () => true)) {
2240
- for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2277
+ for (const dir of list2("app/dashboard", () => true)) {
2278
+ for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2241
2279
  if (existsSync11(join10(appRoot, file))) candidates.push(file);
2242
2280
  }
2243
2281
  }
@@ -2248,20 +2286,20 @@ function findOrphans(appRoot, all) {
2248
2286
  }
2249
2287
  return orphans;
2250
2288
  }
2251
- function ensureSupportFiles(appRoot, log) {
2289
+ function ensureSupportFiles(appRoot, log2) {
2252
2290
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2253
2291
  const target = join10(appRoot, path);
2254
2292
  if (existsSync11(target)) continue;
2255
2293
  mkdirSync4(dirname5(target), { recursive: true });
2256
- copyFileSync(join10(templatesDir, "app", path), target);
2257
- log(`${pc5.green("create".padEnd(9))} ${path}`);
2294
+ copyFileSync2(join10(templatesDir, "app", path), target);
2295
+ log2(`${pc5.green("create".padEnd(9))} ${path}`);
2258
2296
  }
2259
2297
  }
2260
- function logResults(results, log, options2 = {}) {
2298
+ function logResults(results, log2, options2 = {}) {
2261
2299
  for (const { path, outcome } of results) {
2262
2300
  if (outcome === "identical" && !options2.showIdentical) continue;
2263
2301
  const label = outcome === "create" ? pc5.green("create") : outcome === "update" ? pc5.yellow("update") : outcome === "identical" ? pc5.dim("identical") : pc5.red(outcome === "skipped-drift" ? "drift" : "hand-written");
2264
- log(`${label.padEnd(18)} ${path}`);
2302
+ log2(`${label.padEnd(18)} ${path}`);
2265
2303
  }
2266
2304
  }
2267
2305
 
@@ -2296,20 +2334,20 @@ function addStripeDependency(appRoot) {
2296
2334
  writeJson(path, pkg);
2297
2335
  return true;
2298
2336
  }
2299
- function writeBillingResource(appRoot, name, force, log) {
2337
+ function writeBillingResource(appRoot, name, force, log2) {
2300
2338
  const relative2 = descriptorPath(name);
2301
2339
  const path = join11(appRoot, relative2);
2302
2340
  if (!existsSync12(path)) {
2303
2341
  mkdirSync5(dirname6(path), { recursive: true });
2304
2342
  writeFileSync6(path, renderBillingDescriptor(name));
2305
- log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2343
+ log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2306
2344
  return;
2307
2345
  }
2308
2346
  const parts = splitMarkers(readFileSync8(path, "utf8"));
2309
2347
  if (!parts) throw new Error(`${relative2} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
2310
2348
  const merged = mergeBillingFields(name, parts.block);
2311
2349
  if (!merged) {
2312
- log(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2350
+ log2(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2313
2351
  return;
2314
2352
  }
2315
2353
  try {
@@ -2320,22 +2358,22 @@ function writeBillingResource(appRoot, name, force, log) {
2320
2358
  }
2321
2359
  throw error;
2322
2360
  }
2323
- log(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2361
+ log2(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2324
2362
  }
2325
2363
  async function genBilling(options2 = {}) {
2326
- const log = options2.log ?? ((message) => console.log(message));
2364
+ const log2 = options2.log ?? ((message) => console.log(message));
2327
2365
  validateBillingOptions(options2.provider, options2.mode);
2328
2366
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2329
- for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log);
2367
+ for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2330
2368
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2331
2369
  const relative2 = policyPath(name);
2332
2370
  if (existsSync12(join11(appRoot, relative2))) {
2333
- log(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2371
+ log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2334
2372
  continue;
2335
2373
  }
2336
2374
  mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2337
2375
  writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2338
- log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2376
+ log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2339
2377
  }
2340
2378
  const files = [];
2341
2379
  const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
@@ -2343,7 +2381,7 @@ async function genBilling(options2 = {}) {
2343
2381
  const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2344
2382
  files.push({ path, status: "missing", outcome });
2345
2383
  const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2346
- log(`${label.padEnd(18)} ${path}`);
2384
+ log2(`${label.padEnd(18)} ${path}`);
2347
2385
  }
2348
2386
  for (const path of RETIRED_FILES) {
2349
2387
  const absolute = join11(appRoot, path);
@@ -2352,15 +2390,15 @@ async function genBilling(options2 = {}) {
2352
2390
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2353
2391
  if (untouched) {
2354
2392
  unlinkSync(absolute);
2355
- log(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2393
+ log2(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2356
2394
  } else {
2357
- log(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2395
+ log2(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2358
2396
  }
2359
2397
  }
2360
- if (appendVars(appRoot, ".dev.vars.example")) log(`${pc6.green("append".padEnd(9))} .dev.vars.example`);
2361
- if (appendVars(appRoot, ".dev.vars")) log(`${pc6.green("append".padEnd(9))} .dev.vars`);
2398
+ if (appendVars(appRoot, ".dev.vars.example")) log2(`${pc6.green("append".padEnd(9))} .dev.vars.example`);
2399
+ if (appendVars(appRoot, ".dev.vars")) log2(`${pc6.green("append".padEnd(9))} .dev.vars`);
2362
2400
  const addedStripe = addStripeDependency(appRoot);
2363
- if (addedStripe) log(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2401
+ if (addedStripe) log2(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2364
2402
  if (!options2.skipInstall) {
2365
2403
  const pm = detectPackageManager();
2366
2404
  const code = run(pm, ["install"], appRoot);
@@ -2368,16 +2406,16 @@ async function genBilling(options2 = {}) {
2368
2406
  run(pm, ["run", "cf-typegen"], appRoot);
2369
2407
  }
2370
2408
  const all = await loadResources(appRoot);
2371
- ensureSupportFiles(appRoot, log);
2372
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log);
2409
+ ensureSupportFiles(appRoot, log2);
2410
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2373
2411
  let migrations = [];
2374
2412
  if (!options2.skipMigration) {
2375
2413
  const result = await generateSchemaMigration(appRoot, "billing");
2376
2414
  migrations = result.files;
2377
- for (const file of migrations) log(`${pc6.green("create".padEnd(9))} ${file}`);
2378
- for (const repair of result.repairs) log(pc6.dim(`repaired ${repair}`));
2415
+ for (const file of migrations) log2(`${pc6.green("create".padEnd(9))} ${file}`);
2416
+ for (const repair of result.repairs) log2(pc6.dim(`repaired ${repair}`));
2379
2417
  }
2380
- log(
2418
+ log2(
2381
2419
  [
2382
2420
  "",
2383
2421
  `Next:`,
@@ -2519,7 +2557,7 @@ var renderSecurityActions = () => `"use server";
2519
2557
  import { revalidatePath } from "next/cache";
2520
2558
  import { can } from "@flaredev/core";
2521
2559
  import { isIp } from "@flaredev/core/security";
2522
- import { adminSession, policyFor } from "@/lib/admin";
2560
+ import { dashboardSession, policyFor } from "@/lib/dashboard";
2523
2561
  import { logSecurityEvent, security } from "@/lib/security";
2524
2562
 
2525
2563
  /** On failure the submitted values come back, so the form (which React resets) keeps them. */
@@ -2528,7 +2566,7 @@ export type BanResult = { ok: true } | { ok: false; error: string; ip?: string;
2528
2566
  const DURATIONS: Record<string, number | null> = { "1h": 3600, "24h": 86_400, "7d": 604_800, permanent: null };
2529
2567
 
2530
2568
  async function adminOnly(): Promise<{ email: string } | { error: string }> {
2531
- const { session, role } = await adminSession();
2569
+ const { session, role } = await dashboardSession();
2532
2570
  if (!session) return { error: "Sign in required." };
2533
2571
  if (!can(policyFor("SecurityEvent"), role, "create")) return { error: "Only admins can ban or unban." };
2534
2572
  return { email: session.user.email };
@@ -2549,8 +2587,8 @@ export async function banAction(_previous: BanResult | null, form: FormData): Pr
2549
2587
  } catch (error) {
2550
2588
  return { ok: false, error: \`The ban didn't reach every layer: \${error instanceof Error ? error.message : String(error)}\`, ip, reason };
2551
2589
  }
2552
- await logSecurityEvent({ kind: "manual-ban", severity: "medium", ip, path: "/admin/security", userAgent: null, count: 0, banned: true, detail: \`\${reason} (by \${admin.email})\` });
2553
- revalidatePath("/admin/security");
2590
+ await logSecurityEvent({ kind: "manual-ban", severity: "medium", ip, path: "/dashboard/security", userAgent: null, count: 0, banned: true, detail: \`\${reason} (by \${admin.email})\` });
2591
+ revalidatePath("/dashboard/security");
2554
2592
  return { ok: true };
2555
2593
  }
2556
2594
 
@@ -2563,8 +2601,8 @@ export async function unbanAction(ip: string): Promise<BanResult> {
2563
2601
  } catch (error) {
2564
2602
  return { ok: false, error: \`The unban didn't reach every layer: \${error instanceof Error ? error.message : String(error)}\` };
2565
2603
  }
2566
- await logSecurityEvent({ kind: "manual-unban", severity: "low", ip, path: "/admin/security", userAgent: null, count: 0, banned: false, detail: \`by \${admin.email}\` });
2567
- revalidatePath("/admin/security");
2604
+ await logSecurityEvent({ kind: "manual-unban", severity: "low", ip, path: "/dashboard/security", userAgent: null, count: 0, banned: false, detail: \`by \${admin.email}\` });
2605
+ revalidatePath("/dashboard/security");
2568
2606
  return { ok: true };
2569
2607
  }
2570
2608
  `;
@@ -2657,7 +2695,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
2657
2695
  import { Separator } from "@/components/ui/separator";
2658
2696
  import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty";
2659
2697
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
2660
- import { adminStore, policyFor, requireAdmin } from "@/lib/admin";
2698
+ import { dashboardStore, policyFor, requireDashboard } from "@/lib/dashboard";
2661
2699
  import { security, zoneClient } from "@/lib/security";
2662
2700
  import { BanForm, UnbanButton } from "./ban-controls";
2663
2701
 
@@ -2666,7 +2704,7 @@ const when = (value: string | number | Date | null) =>
2666
2704
  value ? new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)) : "Until unbanned";
2667
2705
 
2668
2706
  export default async function SecurityPage() {
2669
- const { role } = await requireAdmin("/admin/security");
2707
+ const { role } = await requireDashboard("/dashboard/security");
2670
2708
  if (!can(policyFor("SecurityEvent"), role, "read")) {
2671
2709
  return (
2672
2710
  <Alert>
@@ -2679,7 +2717,7 @@ export default async function SecurityPage() {
2679
2717
 
2680
2718
  const [bans, events, edge] = await Promise.all([
2681
2719
  security().bans(),
2682
- adminStore("SecurityEvent").list(new URLSearchParams({ sort: "-createdAt", perPage: "20" })),
2720
+ dashboardStore("SecurityEvent").list(new URLSearchParams({ sort: "-createdAt", perPage: "20" })),
2683
2721
  zone
2684
2722
  ? Promise.all([zone.bans(), zone.firewallEvents(new Date(Date.now() - 24 * 3600 * 1000))]).then(
2685
2723
  ([zoneBans, firewall]) => ({ ok: true as const, zoneBans, firewall }),
@@ -2790,7 +2828,7 @@ export default async function SecurityPage() {
2790
2828
  <CardHeader>
2791
2829
  <CardTitle>Recent events</CardTitle>
2792
2830
  <CardDescription>
2793
- Detectors that tripped, and manual bans. <Link href="/admin/security-events" className="underline underline-offset-4">All events</Link>
2831
+ Detectors that tripped, and manual bans. <Link href="/dashboard/security-events" className="underline underline-offset-4">All events</Link>
2794
2832
  </CardDescription>
2795
2833
  </CardHeader>
2796
2834
  <CardContent>
@@ -2900,43 +2938,43 @@ function addSecurityBindings(source) {
2900
2938
  }
2901
2939
  return { text, added };
2902
2940
  }
2903
- var renderAdminNavBlock = () => `export interface AdminLink {
2941
+ var renderAdminNavBlock = () => `export interface DashboardLink {
2904
2942
  label: string;
2905
2943
  href: string;
2906
- /** A resource icon name, e.g. "shield" (see components/admin/resource-icon.tsx). */
2944
+ /** A resource icon name, e.g. "shield" (see components/dashboard/resource-icon.tsx). */
2907
2945
  icon: string;
2908
2946
  }
2909
2947
 
2910
- export const generatedAdminLinks: AdminLink[] = [{ label: "Security", href: "/admin/security", icon: "shield" }];
2948
+ export const generatedDashboardLinks: DashboardLink[] = [{ label: "Security", href: "/dashboard/security", icon: "shield" }];
2911
2949
  `;
2912
2950
 
2913
2951
  // src/commands/gen-security.ts
2914
2952
  function securityFiles(appName) {
2915
2953
  return [
2916
2954
  { path: "lib/security.ts", content: renderLibSecurity(appName) },
2917
- { path: "app/admin/security/page.tsx", content: renderSecurityPage() },
2918
- { path: "app/admin/security/actions.ts", content: renderSecurityActions() },
2919
- { path: "app/admin/security/ban-controls.tsx", content: renderBanControls() }
2955
+ { path: "app/dashboard/security/page.tsx", content: renderSecurityPage() },
2956
+ { path: "app/dashboard/security/actions.ts", content: renderSecurityActions() },
2957
+ { path: "app/dashboard/security/ban-controls.tsx", content: renderBanControls() }
2920
2958
  ];
2921
2959
  }
2922
2960
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2923
- function writeOnce(appRoot, relative2, content, log) {
2961
+ function writeOnce(appRoot, relative2, content, log2) {
2924
2962
  const path = join12(appRoot, relative2);
2925
2963
  if (existsSync13(path)) {
2926
- log(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2964
+ log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2927
2965
  return;
2928
2966
  }
2929
2967
  mkdirSync6(dirname7(path), { recursive: true });
2930
2968
  writeFileSync7(path, content);
2931
- log(`${pc7.green("create".padEnd(9))} ${relative2}`);
2969
+ log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
2932
2970
  }
2933
2971
  async function genSecurity(options2 = {}) {
2934
- const log = options2.log ?? ((message) => console.log(message));
2972
+ const log2 = options2.log ?? ((message) => console.log(message));
2935
2973
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2936
2974
  const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2937
- writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log);
2938
- writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log);
2939
- writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log);
2975
+ writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
2976
+ writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
2977
+ writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
2940
2978
  const files = [];
2941
2979
  const write = (path, content, header) => {
2942
2980
  const absolute = join12(appRoot, path);
@@ -2947,12 +2985,12 @@ async function genSecurity(options2 = {}) {
2947
2985
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
2948
2986
  files.push({ path, status: "missing", outcome });
2949
2987
  const label = outcome === "create" ? pc7.green("create") : outcome === "update" ? pc7.yellow("update") : pc7.dim("identical");
2950
- log(`${label.padEnd(18)} ${path}`);
2988
+ log2(`${label.padEnd(18)} ${path}`);
2951
2989
  };
2952
2990
  for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
2953
- const navPath = join12(appRoot, "lib/admin-nav.ts");
2954
- if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/admin-nav.ts", renderAdminNavBlock());
2955
- else log(pc7.yellow(`lib/admin-nav.ts not found: link to /admin/security from your admin sidebar yourself.`));
2991
+ const navPath = join12(appRoot, "lib/dashboard-nav.ts");
2992
+ if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
2993
+ else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
2956
2994
  const wranglerPath = join12(appRoot, "wrangler.jsonc");
2957
2995
  let bindings = [];
2958
2996
  if (existsSync13(wranglerPath)) {
@@ -2960,27 +2998,27 @@ async function genSecurity(options2 = {}) {
2960
2998
  bindings = added;
2961
2999
  if (added.length) {
2962
3000
  writeFileSync7(wranglerPath, text);
2963
- for (const line of added) log(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
3001
+ for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2964
3002
  }
2965
3003
  } else {
2966
- log(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
3004
+ log2(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2967
3005
  }
2968
3006
  const all = await loadResources(appRoot);
2969
- ensureSupportFiles(appRoot, log);
2970
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log);
3007
+ ensureSupportFiles(appRoot, log2);
3008
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2971
3009
  let migrations = [];
2972
3010
  if (!options2.skipMigration) {
2973
3011
  const result = await generateSchemaMigration(appRoot, "security");
2974
3012
  migrations = result.files;
2975
- for (const file of migrations) log(`${pc7.green("create".padEnd(9))} ${file}`);
2976
- for (const repair of result.repairs) log(pc7.dim(`repaired ${repair}`));
3013
+ for (const file of migrations) log2(`${pc7.green("create".padEnd(9))} ${file}`);
3014
+ for (const repair of result.repairs) log2(pc7.dim(`repaired ${repair}`));
2977
3015
  }
2978
- log(
3016
+ log2(
2979
3017
  [
2980
3018
  "",
2981
3019
  `Next:`,
2982
3020
  ` 1. ${pc7.bold("flare migrate")} to create the security_events table.`,
2983
- ` 2. ${pc7.bold("flare dev")}, then open ${pc7.bold("/admin/security")}. Tune detectors in security.config.ts.`,
3021
+ ` 2. ${pc7.bold("flare dev")}, then open ${pc7.bold("/dashboard/security")}. Tune detectors in security.config.ts.`,
2984
3022
  ` 3. Optional zone layer: deploy with FLARE_SECURITY_ZONE_ID and FLARE_SECURITY_API_TOKEN set (a token scoped to that`,
2985
3023
  ` one zone with Zone WAF Edit, Firewall Services Edit, Analytics Read, Zone Read). ${pc7.bold("flare deploy")} pushes the`,
2986
3024
  ` zone rules and uploads both as secrets.`
@@ -2994,7 +3032,7 @@ import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSy
2994
3032
  import { join as join13 } from "path";
2995
3033
  import pc8 from "picocolors";
2996
3034
  var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
2997
- function setTheme(name, cwd = process.cwd(), log = console.log) {
3035
+ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
2998
3036
  const appRoot = findAppRoot(cwd);
2999
3037
  const path = join13(appRoot, "lib/site.ts");
3000
3038
  if (!existsSync14(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
@@ -3002,34 +3040,50 @@ function setTheme(name, cwd = process.cwd(), log = console.log) {
3002
3040
  const current = THEME_LINE.exec(source)?.[1];
3003
3041
  if (!name) {
3004
3042
  for (const [theme2, description] of Object.entries(THEMES)) {
3005
- log(`${theme2 === current ? pc8.green("\u25CF") : " "} ${theme2.padEnd(8)} ${pc8.dim(description)}`);
3043
+ log2(`${theme2 === current ? pc8.green("\u25CF") : " "} ${theme2.padEnd(8)} ${pc8.dim(description)}`);
3006
3044
  }
3007
- log(pc8.dim("\nSwitch with `flare theme <name>`. FLARE_THEME in .env overrides it for a build."));
3045
+ log2(pc8.dim("\nSwitch with `flare theme <name>`. FLARE_THEME in .env overrides it for a build."));
3008
3046
  return;
3009
3047
  }
3010
3048
  const theme = parseTheme(name);
3011
3049
  if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
3012
3050
  writeFileSync8(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
3013
- log(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3051
+ log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3014
3052
  }
3015
3053
 
3016
3054
  // src/commands/create-prompts.ts
3017
- import * as prompts from "@clack/prompts";
3055
+ import * as prompts2 from "@clack/prompts";
3056
+ import pc9 from "picocolors";
3057
+ import { FLARE_VERSION as FLARE_VERSION2 } from "@flaredev/core";
3018
3058
  var canPrompt = (yes) => !yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
3059
+ var AUTH_HINTS = {
3060
+ "magic-link": "a sign-in link by email",
3061
+ "email-otp": "a 6-digit sign-in code by email",
3062
+ passkeys: "Face ID, Touch ID, Windows Hello",
3063
+ "2fa-app": "authenticator app, with backup codes",
3064
+ "2fa-email": "a code by email as the second step"
3065
+ };
3066
+ var AUTH_LABELS = {
3067
+ "magic-link": "Magic links",
3068
+ "email-otp": "Email codes",
3069
+ passkeys: "Passkeys",
3070
+ "2fa-app": "Two-factor: authenticator app",
3071
+ "2fa-email": "Two-factor: email code"
3072
+ };
3019
3073
  function stopIfCancelled(value) {
3020
- if (prompts.isCancel(value)) {
3021
- prompts.cancel("Cancelled: nothing was created.");
3074
+ if (prompts2.isCancel(value)) {
3075
+ prompts2.cancel("Cancelled: nothing was created.");
3022
3076
  process.exit(1);
3023
3077
  }
3024
3078
  return value;
3025
3079
  }
3026
- async function askCreateQuestions(given) {
3027
- prompts.intro("Let's set up your Flare app");
3080
+ async function askCreateQuestions(given, appName) {
3081
+ prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
3028
3082
  const answers = { ...given };
3029
3083
  if (given.theme === void 0) {
3030
3084
  answers.theme = stopIfCancelled(
3031
- await prompts.select({
3032
- message: "Pick a look (change it later in lib/site.ts or with FLARE_THEME)",
3085
+ await prompts2.select({
3086
+ message: "Pick a look",
3033
3087
  initialValue: "default",
3034
3088
  options: Object.entries(THEMES).map(([value, hint]) => ({ value, label: value, hint }))
3035
3089
  })
@@ -3037,19 +3091,23 @@ async function askCreateQuestions(given) {
3037
3091
  }
3038
3092
  if (given.auth === void 0) {
3039
3093
  const methods = stopIfCancelled(
3040
- await prompts.multiselect({
3041
- message: "How can people sign in? Email and password is always on. (space to toggle)",
3094
+ await prompts2.multiselect({
3095
+ message: `How can people sign in? ${pc9.dim("Email and password is always on")}`,
3042
3096
  initialValues: DEFAULT_AUTH_METHODS,
3043
3097
  required: false,
3044
- options: Object.entries(AUTH_METHODS).map(([value, label]) => ({ value, label }))
3098
+ options: Object.keys(AUTH_METHODS).map((value) => ({
3099
+ value,
3100
+ label: AUTH_LABELS[value],
3101
+ hint: AUTH_HINTS[value]
3102
+ }))
3045
3103
  })
3046
3104
  );
3047
3105
  answers.auth = methods.length ? methods.join(",") : "none";
3048
3106
  }
3049
3107
  if (given.authProviders === void 0) {
3050
3108
  const providers = stopIfCancelled(
3051
- await prompts.multiselect({
3052
- message: "Social sign-in? Each needs its own credentials later. (space to toggle, enter to skip)",
3109
+ await prompts2.multiselect({
3110
+ message: `Social sign-in? ${pc9.dim("each needs its own credentials later")}`,
3053
3111
  initialValues: [],
3054
3112
  required: false,
3055
3113
  options: Object.entries(AUTH_PROVIDERS).map(([value, { label }]) => ({ value, label }))
@@ -3057,7 +3115,6 @@ async function askCreateQuestions(given) {
3057
3115
  );
3058
3116
  answers.authProviders = providers.join(",");
3059
3117
  }
3060
- prompts.outro("Creating your app");
3061
3118
  return answers;
3062
3119
  }
3063
3120
 
@@ -3065,8 +3122,8 @@ async function askCreateQuestions(given) {
3065
3122
  import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
3066
3123
  import { dirname as dirname8, join as join14 } from "path";
3067
3124
  import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
3068
- import pc9 from "picocolors";
3069
- function validateFields(name, fields, others, log) {
3125
+ import pc10 from "picocolors";
3126
+ function validateFields(name, fields, others, log2) {
3070
3127
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
3071
3128
  for (const field of fields) {
3072
3129
  if (field.kind === "belongsTo" && !known.has(field.target)) {
@@ -3076,11 +3133,11 @@ function validateFields(name, fields, others, log) {
3076
3133
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
3077
3134
  const graph = relationGraph2([...others, candidate]);
3078
3135
  for (const pending of graph.pending.filter((p) => p.resource === name)) {
3079
- log(pc9.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
3136
+ log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
3080
3137
  }
3081
3138
  }
3082
3139
  async function genResource(rawName, options2) {
3083
- const log = options2.log ?? ((message) => console.log(message));
3140
+ const log2 = options2.log ?? ((message) => console.log(message));
3084
3141
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3085
3142
  const name = resourceName(rawName);
3086
3143
  const relativePath = descriptorPath(name);
@@ -3093,15 +3150,15 @@ async function genResource(rawName, options2) {
3093
3150
  const others = existing.filter(({ resource }) => resource.name !== name).map(({ resource }) => resource);
3094
3151
  if (options2.fields) {
3095
3152
  const fields = parseFields(options2.fields);
3096
- validateFields(name, fields, others, log);
3153
+ validateFields(name, fields, others, log2);
3097
3154
  if (!exists) {
3098
3155
  mkdirSync7(dirname8(path), { recursive: true });
3099
- writeFileSync9(path, renderDescriptor(name, fields));
3100
- log(`${pc9.green("create".padEnd(9))} ${relativePath}`);
3156
+ writeFileSync9(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
3157
+ log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
3101
3158
  } else {
3102
3159
  try {
3103
3160
  const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
3104
- log(`${(outcome === "identical" ? pc9.dim : pc9.yellow)(outcome.padEnd(9))} ${relativePath}`);
3161
+ log2(`${(outcome === "identical" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relativePath}`);
3105
3162
  } catch (error) {
3106
3163
  if (error instanceof DriftError) {
3107
3164
  throw new Error(
@@ -3117,9 +3174,9 @@ async function genResource(rawName, options2) {
3117
3174
  try {
3118
3175
  const all = await loadResources(appRoot);
3119
3176
  table = all.find(({ resource }) => resource.name === name).resource.table;
3120
- ensureSupportFiles(appRoot, log);
3177
+ ensureSupportFiles(appRoot, log2);
3121
3178
  written = applyPlan(appRoot, planFiles(all), { force: options2.force });
3122
- logResults(written, log);
3179
+ logResults(written, log2);
3123
3180
  } catch (error) {
3124
3181
  const hint = exists ? "" : `
3125
3182
  (${relativePath} was written; fix it and run \`flare sync-types\`.)`;
@@ -3127,75 +3184,138 @@ async function genResource(rawName, options2) {
3127
3184
  }
3128
3185
  const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
3129
3186
  if (skipped.length) {
3130
- log(pc9.yellow(`
3187
+ log2(pc10.yellow(`
3131
3188
  Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
3132
3189
  }
3133
3190
  let migrations = [];
3134
3191
  if (!options2.skipMigration) {
3135
3192
  const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3136
3193
  migrations = result.files;
3137
- for (const file of migrations) log(`${pc9.green("create".padEnd(9))} ${file}`);
3138
- for (const repair of result.repairs) log(pc9.dim(`repaired ${repair}`));
3139
- if (migrations.length) log(`
3140
- Next: ${pc9.bold("flare migrate")} to apply it locally.`);
3141
- else if (exists) log(pc9.dim("\nNo table changes, so no migration."));
3194
+ for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3195
+ for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3196
+ if (migrations.length) log2(`
3197
+ Next: ${pc10.bold("flare migrate")} to apply it locally.`);
3198
+ else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3142
3199
  }
3143
3200
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
3144
3201
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
3145
3202
  }
3146
3203
 
3147
- // src/commands/gen-migration.ts
3204
+ // src/commands/gen-endpoint.ts
3148
3205
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
3149
- import { basename as basename2, join as join15 } from "path";
3206
+ import { join as join15 } from "path";
3207
+ import { kebabCase as kebabCase3, pascalCase as pascalCase2 } from "@flaredev/core";
3208
+ import pc11 from "picocolors";
3209
+ var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
3210
+ var ACTIONS = ["read", "create", "update", "delete"];
3211
+ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3212
+ const log2 = options2.log ?? ((message) => console.log(message));
3213
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3214
+ const method = (options2.method ?? "GET").toUpperCase();
3215
+ if (!METHODS.includes(method)) throw new Error(`Unknown method "${method}". Use one of ${METHODS.join(", ")}.`);
3216
+ const name = kebabCase3(rawName);
3217
+ if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
3218
+ const resources = await loadResources(appRoot);
3219
+ const wanted = pascalCase2(resourceName2);
3220
+ const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
3221
+ if (!entry) {
3222
+ throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
3223
+ }
3224
+ const action = options2.action ?? (method === "GET" ? "read" : "update");
3225
+ if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3226
+ const { resource, stem } = entry;
3227
+ const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3228
+ const path = join15(appRoot, relative2);
3229
+ if (existsSync16(path)) throw new Error(`${relative2} already exists.`);
3230
+ mkdirSync8(join15(path, ".."), { recursive: true });
3231
+ const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3232
+ writeFileSync10(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3233
+ log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3234
+ log2(`
3235
+ It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
3236
+ return relative2;
3237
+ }
3238
+ function renderEndpoint({ resource, stem, method, action, record, route }) {
3239
+ const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
3240
+ const params = record ? ", { params }: { params: Promise<{ id: string }> }" : "";
3241
+ const id = record ? " const { id } = await params;\n" : "";
3242
+ return `import { authorize, currentUser } from "@/lib/api";
3243
+ import { dashboardStore } from "@/lib/dashboard";
3244
+ import ${stem.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())}Resource from "@/resources/${stem}.resource";
3245
+
3246
+ /**
3247
+ * ${method} ${route}
3248
+ *
3249
+ * Yours to change. The session and the policy are checked the same way the generated
3250
+ * CRUD routes check them, and \`store\` is the same store they use \u2014 so hooks, validation
3251
+ * and cache invalidation all still apply.
3252
+ */
3253
+ export async function ${method}(request: Request${params}) {
3254
+ ${id} const denied = await authorize({ request, resource: ${stem.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())}Resource, action: "${action}" });
3255
+ if (denied) return denied;
3256
+
3257
+ const user = await currentUser();
3258
+ const store = dashboardStore("${resource}");
3259
+ ${body}
3260
+ // Your logic here. \`store.list\`, \`store.get\`, \`store.create\`, \`store.update\` and
3261
+ // \`store.delete\` are available, or reach for Drizzle directly with \`getDb()\`.
3262
+ return Response.json({ ok: true, ${record ? "id, " : ""}${method === "GET" || method === "DELETE" ? "" : "received: body, "}user: user?.email ?? null });
3263
+ }
3264
+ `;
3265
+ }
3266
+
3267
+ // src/commands/gen-migration.ts
3268
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
3269
+ import { basename as basename2, join as join16 } from "path";
3150
3270
  import { snakeCase } from "@flaredev/core";
3151
- import pc10 from "picocolors";
3271
+ import pc12 from "picocolors";
3152
3272
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
3153
3273
  -- Write SQL that undoes the up migration, e.g.:
3154
3274
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
3155
3275
  -- While this file has no statements, rollback of ${up} is refused.
3156
3276
  `;
3157
3277
  async function genMigration(rawName, options2 = {}) {
3158
- const log = options2.log ?? ((message) => console.log(message));
3278
+ const log2 = options2.log ?? ((message) => console.log(message));
3159
3279
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3160
3280
  const name = snakeCase(rawName);
3161
3281
  if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
3162
3282
  const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
3163
3283
  if (files.length === 0) {
3164
- log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
3284
+ log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
3165
3285
  return [];
3166
3286
  }
3167
- for (const file of files) log(`${pc10.green("create".padEnd(9))} ${file}`);
3168
- for (const repair of repairs) log(pc10.dim(`repaired ${repair}`));
3287
+ for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
3288
+ for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
3169
3289
  if (!options2.fromSchema) {
3170
- const downDir = join15(appRoot, "migrations", "down");
3171
- mkdirSync8(downDir, { recursive: true });
3290
+ const downDir = join16(appRoot, "migrations", "down");
3291
+ mkdirSync9(downDir, { recursive: true });
3172
3292
  for (const file of files) {
3173
- const down = join15(downDir, basename2(file));
3174
- if (existsSync16(down)) continue;
3175
- writeFileSync10(down, DOWN_TEMPLATE(basename2(file)));
3176
- log(`${pc10.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3293
+ const down = join16(downDir, basename2(file));
3294
+ if (existsSync17(down)) continue;
3295
+ writeFileSync11(down, DOWN_TEMPLATE(basename2(file)));
3296
+ log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3177
3297
  }
3178
- log(`
3179
- Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc10.bold("flare migrate")}.`);
3298
+ log2(`
3299
+ Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc12.bold("flare migrate")}.`);
3180
3300
  } else {
3181
- log(`
3182
- Next: ${pc10.bold("flare migrate")}.`);
3301
+ log2(`
3302
+ Next: ${pc12.bold("flare migrate")}.`);
3183
3303
  }
3184
3304
  return files;
3185
3305
  }
3186
3306
 
3187
3307
  // src/commands/gen-policy.ts
3188
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
3189
- import { dirname as dirname9, join as join16 } from "path";
3308
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
3309
+ import { dirname as dirname9, join as join17 } from "path";
3190
3310
  import { definePolicy } from "@flaredev/core";
3191
- import pc11 from "picocolors";
3311
+ import pc13 from "picocolors";
3192
3312
  function parseRoles(input, what = "--roles") {
3193
3313
  const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
3194
3314
  if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
3195
3315
  return [...new Set(roles)];
3196
3316
  }
3197
3317
  async function genPolicy(rawName, options2) {
3198
- const log = options2.log ?? ((message) => console.log(message));
3318
+ const log2 = options2.log ?? ((message) => console.log(message));
3199
3319
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3200
3320
  const name = resourceName(rawName);
3201
3321
  const resources = await loadResources(appRoot);
@@ -3204,8 +3324,8 @@ async function genPolicy(rawName, options2) {
3204
3324
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3205
3325
  }
3206
3326
  const relative2 = policyPath(name);
3207
- const path = join16(appRoot, relative2);
3208
- const exists = existsSync17(path);
3327
+ const path = join17(appRoot, relative2);
3328
+ const exists = existsSync18(path);
3209
3329
  let roles;
3210
3330
  let deleteRoles;
3211
3331
  if (options2.roles || !exists) {
@@ -3216,13 +3336,13 @@ async function genPolicy(rawName, options2) {
3216
3336
  if (roles && deleteRoles) {
3217
3337
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
3218
3338
  if (!exists) {
3219
- mkdirSync9(dirname9(path), { recursive: true });
3220
- writeFileSync11(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3221
- log(`${pc11.green("create".padEnd(9))} ${relative2}`);
3339
+ mkdirSync10(dirname9(path), { recursive: true });
3340
+ writeFileSync12(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3341
+ log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3222
3342
  } else {
3223
3343
  try {
3224
3344
  const outcome = writeGenerated(path, block, { force: options2.force });
3225
- log(`${(outcome === "identical" ? pc11.dim : pc11.yellow)(outcome.padEnd(9))} ${relative2}`);
3345
+ log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative2}`);
3226
3346
  } catch (error) {
3227
3347
  if (error instanceof DriftError) {
3228
3348
  throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
@@ -3232,17 +3352,17 @@ async function genPolicy(rawName, options2) {
3232
3352
  }
3233
3353
  }
3234
3354
  const policies = await loadPolicies(appRoot);
3235
- logResults(applyPlan(appRoot, planFiles(resources, policies)), log);
3236
- log(`
3237
- ${pc11.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3355
+ logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
3356
+ log2(`
3357
+ ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3238
3358
  return { name, path: relative2 };
3239
3359
  }
3240
3360
 
3241
3361
  // src/commands/migrate.ts
3242
- import { existsSync as existsSync18, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
3362
+ import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
3243
3363
  import { tmpdir } from "os";
3244
- import { join as join17 } from "path";
3245
- import pc12 from "picocolors";
3364
+ import { join as join18 } from "path";
3365
+ import pc14 from "picocolors";
3246
3366
 
3247
3367
  // src/generator/down.ts
3248
3368
  var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
@@ -3358,18 +3478,18 @@ ${result.output.trim()}`);
3358
3478
  async function rollback(options2 = {}) {
3359
3479
  const steps = options2.steps ?? 1;
3360
3480
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3361
- const { appRoot, db, target, where, wrangler, log } = context(options2);
3362
- const migrationsDir = join17(appRoot, db.migrations_dir ?? "migrations");
3481
+ const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
3482
+ const migrationsDir = join18(appRoot, db.migrations_dir ?? "migrations");
3363
3483
  const applied = await appliedMigrations(options2, steps);
3364
3484
  if (applied.length === 0) {
3365
- log("Nothing to roll back.");
3485
+ log2("Nothing to roll back.");
3366
3486
  return 0;
3367
3487
  }
3368
3488
  const plan = [];
3369
3489
  for (const { name } of applied) {
3370
3490
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
3371
- const handWritten = join17(migrationsDir, "down", name);
3372
- if (existsSync18(handWritten)) {
3491
+ const handWritten = join18(migrationsDir, "down", name);
3492
+ if (existsSync19(handWritten)) {
3373
3493
  const sql2 = readFileSync11(handWritten, "utf8");
3374
3494
  if (splitStatements(sql2).length === 0) {
3375
3495
  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.`);
@@ -3377,8 +3497,8 @@ async function rollback(options2 = {}) {
3377
3497
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3378
3498
  continue;
3379
3499
  }
3380
- const upPath = join17(migrationsDir, name);
3381
- if (!existsSync18(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3500
+ const upPath = join18(migrationsDir, name);
3501
+ if (!existsSync19(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3382
3502
  const down = deriveDown(readFileSync11(upPath, "utf8"));
3383
3503
  if (!down.reversible) {
3384
3504
  throw new Error(
@@ -3388,37 +3508,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3388
3508
  }
3389
3509
  plan.push({ name, sql: down.sql, source: "derived" });
3390
3510
  }
3391
- log(`${pc12.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3511
+ log2(`${pc14.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3392
3512
  for (const step of plan) {
3393
- log(` ${step.name} ${pc12.dim(`(${step.source})`)}`);
3394
- for (const line of step.sql.trim().split("\n")) log(pc12.dim(` ${line}`));
3513
+ log2(` ${step.name} ${pc14.dim(`(${step.source})`)}`);
3514
+ for (const line of step.sql.trim().split("\n")) log2(pc14.dim(` ${line}`));
3395
3515
  }
3396
3516
  if (options2.remote && !options2.yes) {
3397
- log(pc12.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3517
+ log2(pc14.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3398
3518
  return 1;
3399
3519
  }
3400
3520
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3401
3521
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3402
- const dir = mkdtempSync(join17(tmpdir(), "flare-rollback-"));
3522
+ const dir = mkdtempSync(join18(tmpdir(), "flare-rollback-"));
3403
3523
  try {
3404
- const file = join17(dir, "rollback.sql");
3405
- writeFileSync12(file, script);
3524
+ const file = join18(dir, "rollback.sql");
3525
+ writeFileSync13(file, script);
3406
3526
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3407
3527
  if (result.code !== 0) throw new Error(`Rollback failed:
3408
3528
  ${result.output.trim()}`);
3409
3529
  } finally {
3410
3530
  rmSync2(dir, { recursive: true, force: true });
3411
3531
  }
3412
- log(pc12.green(`
3532
+ log2(pc14.green(`
3413
3533
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3414
3534
  return 0;
3415
3535
  }
3416
3536
 
3417
3537
  // src/commands/rm.ts
3418
- import { existsSync as existsSync19, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3419
- import { dirname as dirname10, join as join18 } from "path";
3538
+ import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3539
+ import { dirname as dirname10, join as join19 } from "path";
3420
3540
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3421
- import pc13 from "picocolors";
3541
+ import pc15 from "picocolors";
3422
3542
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
3423
3543
  function handWrittenReason(source, header, options2 = {}) {
3424
3544
  const parts = splitMarkers(source);
@@ -3435,16 +3555,16 @@ function handWrittenReason(source, header, options2 = {}) {
3435
3555
  return null;
3436
3556
  }
3437
3557
  function removeEmptyDirs(appRoot, start) {
3438
- let dir = dirname10(join18(appRoot, start));
3439
- const roots = [join18(appRoot, "app", "api"), join18(appRoot, "app", "admin")];
3558
+ let dir = dirname10(join19(appRoot, start));
3559
+ const roots = [join19(appRoot, "app", "api"), join19(appRoot, "app", "dashboard")];
3440
3560
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3441
- while (inside() && existsSync19(dir) && readdirSync7(dir).length === 0) {
3561
+ while (inside() && existsSync20(dir) && readdirSync7(dir).length === 0) {
3442
3562
  rmdirSync(dir);
3443
3563
  dir = dirname10(dir);
3444
3564
  }
3445
3565
  }
3446
3566
  async function rmResource(rawName, options2 = {}) {
3447
- const log = options2.log ?? ((message) => console.log(message));
3567
+ const log2 = options2.log ?? ((message) => console.log(message));
3448
3568
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3449
3569
  const name = resourceName(rawName);
3450
3570
  const all = await loadResources(appRoot);
@@ -3467,7 +3587,7 @@ async function rmResource(rawName, options2 = {}) {
3467
3587
  if (entry) {
3468
3588
  owned.add(entry.file);
3469
3589
  for (const file of planFiles(all)) {
3470
- if (existsSync19(join18(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3590
+ if (existsSync20(join19(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3471
3591
  }
3472
3592
  }
3473
3593
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3476,7 +3596,7 @@ async function rmResource(rawName, options2 = {}) {
3476
3596
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3477
3597
  const findings = [];
3478
3598
  for (const path of owned) {
3479
- const source = readFileSync12(join18(appRoot, path), "utf8");
3599
+ const source = readFileSync12(join19(appRoot, path), "utf8");
3480
3600
  let wrapper;
3481
3601
  if (entry && path === entry.file) {
3482
3602
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3497,34 +3617,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
3497
3617
  }
3498
3618
  const removed = [...owned].sort();
3499
3619
  for (const path of removed) {
3500
- unlinkSync2(join18(appRoot, path));
3620
+ unlinkSync2(join19(appRoot, path));
3501
3621
  removeEmptyDirs(appRoot, path);
3502
- log(`${pc13.red("remove".padEnd(9))} ${path}`);
3622
+ log2(`${pc15.red("remove".padEnd(9))} ${path}`);
3503
3623
  }
3504
- logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
3624
+ logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
3505
3625
  const table = entry ? tableExport(entry.resource) : void 0;
3506
- if (table && existsSync19(join18(appRoot, "seeds"))) {
3507
- const seeds = readdirSync7(join18(appRoot, "seeds")).filter(
3508
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join18(appRoot, "seeds", file), "utf8"))
3626
+ if (table && existsSync20(join19(appRoot, "seeds"))) {
3627
+ const seeds = readdirSync7(join19(appRoot, "seeds")).filter(
3628
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join19(appRoot, "seeds", file), "utf8"))
3509
3629
  );
3510
- if (seeds.length) log(pc13.yellow(`
3630
+ if (seeds.length) log2(pc15.yellow(`
3511
3631
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
3512
3632
  }
3513
3633
  let migrations = [];
3514
3634
  if (!options2.skipMigration && entry) {
3515
3635
  const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
3516
3636
  migrations = result.files;
3517
- for (const file of migrations) log(`${pc13.green("create".padEnd(9))} ${file}`);
3518
- for (const repair of result.repairs) log(pc13.dim(`repaired ${repair}`));
3519
- if (migrations.length) log(`
3520
- Next: ${pc13.bold("flare migrate")} to drop the table locally.`);
3637
+ for (const file of migrations) log2(`${pc15.green("create".padEnd(9))} ${file}`);
3638
+ for (const repair of result.repairs) log2(pc15.dim(`repaired ${repair}`));
3639
+ if (migrations.length) log2(`
3640
+ Next: ${pc15.bold("flare migrate")} to drop the table locally.`);
3521
3641
  }
3522
3642
  return { removed, migrations };
3523
3643
  }
3524
3644
 
3525
3645
  // src/commands/role.ts
3526
3646
  import { humanize } from "@flaredev/core";
3527
- import pc14 from "picocolors";
3647
+ import pc16 from "picocolors";
3528
3648
  var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
3529
3649
  function roleInsertSql(name, label) {
3530
3650
  if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
@@ -3557,7 +3677,7 @@ ${result.output.trim()}`);
3557
3677
  return (first?.results ?? []).map((row) => row.name);
3558
3678
  }
3559
3679
  async function addRole(name, options2 = {}) {
3560
- const { appRoot, binding, wrangler, target, log } = context2(options2);
3680
+ const { appRoot, binding, wrangler, target, log: log2 } = context2(options2);
3561
3681
  const sql2 = roleInsertSql(name, options2.label ?? humanize(name));
3562
3682
  const result = await runWrangler(wrangler, ["d1", "execute", binding, ...target, "--json", "--command", sql2], appRoot, { capture: true });
3563
3683
  if (result.code !== 0) {
@@ -3569,24 +3689,137 @@ ${result.output.trim()}`);
3569
3689
  }
3570
3690
  const [first] = extractJson(result.output);
3571
3691
  const added = first?.results?.[0];
3572
- log(pc14.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3573
- log(pc14.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3692
+ log2(pc16.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3693
+ log2(pc16.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3574
3694
  return 0;
3575
3695
  }
3576
3696
 
3577
3697
  // src/commands/seed.ts
3578
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync13 } from "fs";
3698
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "fs";
3699
+ import { join as join21 } from "path";
3700
+ import { createFake, kebabCase as kebabCase4, storedFields as storedFields2 } from "@flaredev/core";
3701
+ import { createJiti as createJiti4 } from "jiti";
3702
+ import pc17 from "picocolors";
3703
+
3704
+ // src/seed/bulk.ts
3705
+ var MAX_STATEMENT_BYTES = 8e4;
3706
+ var STATEMENTS_PER_CALL = 25;
3707
+ function sqlLiteral(value) {
3708
+ if (value === null || value === void 0) return "NULL";
3709
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
3710
+ if (typeof value === "boolean") return value ? "1" : "0";
3711
+ if (value instanceof Date) return String(value.getTime());
3712
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
3713
+ return `'${text.replaceAll("'", "''")}'`;
3714
+ }
3715
+ var tuple = (row, columns) => `(${columns.map((column) => sqlLiteral(row[column])).join(",")})`;
3716
+ function* insertStatements(table, columns, rows, maxBytes = MAX_STATEMENT_BYTES) {
3717
+ const head = `INSERT INTO "${table}" (${columns.map((column) => `"${column}"`).join(",")}) VALUES `;
3718
+ let values = [];
3719
+ let bytes = head.length;
3720
+ for (const row of rows) {
3721
+ const text = tuple(row, columns);
3722
+ if (values.length > 0 && bytes + text.length + 1 > maxBytes) {
3723
+ yield { sql: `${head}${values.join(",")};`, rows: values.length };
3724
+ values = [];
3725
+ bytes = head.length;
3726
+ }
3727
+ values.push(text);
3728
+ bytes += text.length + 1;
3729
+ }
3730
+ if (values.length > 0) yield { sql: `${head}${values.join(",")};`, rows: values.length };
3731
+ }
3732
+ async function bulkInsert(sink, options2) {
3733
+ const perCall = options2.statementsPerCall ?? STATEMENTS_PER_CALL;
3734
+ let written = 0;
3735
+ let batch = [];
3736
+ let batchRows = 0;
3737
+ const flush = async () => {
3738
+ if (batch.length === 0) return;
3739
+ await sink.exec(batch.join("\n"));
3740
+ written += batchRows;
3741
+ batch = [];
3742
+ batchRows = 0;
3743
+ options2.onProgress?.(written);
3744
+ };
3745
+ for (const statement of insertStatements(options2.table, options2.columns, options2.rows, options2.maxBytes)) {
3746
+ batch.push(statement.sql);
3747
+ batchRows += statement.rows;
3748
+ if (batch.length === perCall) await flush();
3749
+ }
3750
+ await flush();
3751
+ return written;
3752
+ }
3753
+
3754
+ // src/seed/insert-many.ts
3755
+ var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
3756
+ function createInsertMany(sink, drizzle, onProgress) {
3757
+ return async function insertMany(table, count, build) {
3758
+ const name = drizzle.getTableName(table);
3759
+ const columns = drizzle.getTableColumns(table);
3760
+ if (typeof count === "number" && !build) throw new Error(`insertMany(${name}, ${count}) needs a function to build each row.`);
3761
+ const source = typeof count === "number" ? (function* () {
3762
+ for (let i = 0; i < count; i++) yield build(i);
3763
+ })() : isIterable(count) ? count : [];
3764
+ const iterator = source[Symbol.iterator]();
3765
+ const first = iterator.next();
3766
+ if (first.done) return 0;
3767
+ const keys = new Set(Object.keys(first.value));
3768
+ for (const [key, column] of Object.entries(columns)) if (column.defaultFn) keys.add(key);
3769
+ const used = Object.keys(columns).filter((key) => keys.has(key));
3770
+ const unknownKeys = [...keys].filter((key) => !columns[key]);
3771
+ if (unknownKeys.length) throw new Error(`${name} has no column called ${unknownKeys.join(", ")}.`);
3772
+ const toRow = (value) => {
3773
+ const row = {};
3774
+ for (const key of used) {
3775
+ const column = columns[key];
3776
+ let raw = value[key];
3777
+ if (raw === void 0 && column.defaultFn) raw = column.defaultFn();
3778
+ row[column.name] = raw === void 0 || raw === null ? null : column.mapToDriverValue?.(raw) ?? raw;
3779
+ }
3780
+ return row;
3781
+ };
3782
+ const rows = (function* () {
3783
+ yield toRow(first.value);
3784
+ for (let next = iterator.next(); !next.done; next = iterator.next()) yield toRow(next.value);
3785
+ })();
3786
+ return bulkInsert(sink, {
3787
+ table: name,
3788
+ columns: used.map((key) => columns[key].name),
3789
+ rows,
3790
+ onProgress: onProgress && ((written) => onProgress(written, name))
3791
+ });
3792
+ };
3793
+ }
3794
+
3795
+ // src/seed/local-d1.ts
3579
3796
  import { createRequire } from "module";
3580
- import { join as join19 } from "path";
3797
+ import { join as join20 } from "path";
3581
3798
  import { pathToFileURL } from "url";
3582
- import { kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
3583
- import { createJiti as createJiti4 } from "jiti";
3584
- import pc15 from "picocolors";
3585
- var SEEDS_DIR = "seeds";
3586
3799
  var LOCAL_STATE = ".wrangler/state/v3";
3800
+ async function openLocalD1(appRoot, binding) {
3801
+ process.env.WRANGLER_LOG ??= "error";
3802
+ const appRequire = createRequire(join20(appRoot, "package.json"));
3803
+ const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3804
+ const { env, dispose } = await wrangler.getPlatformProxy({
3805
+ configPath: join20(appRoot, "wrangler.jsonc"),
3806
+ persist: { path: join20(appRoot, LOCAL_STATE) }
3807
+ });
3808
+ const bindings = env;
3809
+ const name = binding ?? "DB";
3810
+ const db = bindings[name];
3811
+ if (!db) {
3812
+ const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
3813
+ throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
3814
+ }
3815
+ return { db, env: bindings, dispose };
3816
+ }
3817
+
3818
+ // src/commands/seed.ts
3819
+ var SEEDS_DIR = "seeds";
3587
3820
  function seedFiles(appRoot, names) {
3588
- const dir = join19(appRoot, SEEDS_DIR);
3589
- const all = existsSync20(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3821
+ const dir = join21(appRoot, SEEDS_DIR);
3822
+ const all = existsSync21(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3590
3823
  if (!names?.length) return all;
3591
3824
  return names.map((name) => {
3592
3825
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3597,65 +3830,66 @@ function seedFiles(appRoot, names) {
3597
3830
  });
3598
3831
  }
3599
3832
  async function runSeeds(options2 = {}) {
3600
- const log = options2.log ?? ((message) => console.log(message));
3833
+ const log2 = options2.log ?? ((message) => console.log(message));
3601
3834
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3602
3835
  const files = seedFiles(appRoot, options2.names);
3603
3836
  if (files.length === 0) {
3604
- log(`No seeds found. Create one with ${pc15.bold("flare seed:make <name>")}.`);
3837
+ log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
3605
3838
  return;
3606
3839
  }
3607
- const jiti = createJiti4(join19(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3608
- const appRequire = createRequire(join19(appRoot, "package.json"));
3609
- const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3840
+ const jiti = createJiti4(join21(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3610
3841
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3611
- const schema = await jiti.import(join19(appRoot, "db/schema.ts"));
3612
- const { env, dispose } = await wrangler.getPlatformProxy({
3613
- configPath: join19(appRoot, "wrangler.jsonc"),
3614
- persist: { path: join19(appRoot, LOCAL_STATE) }
3615
- });
3842
+ const drizzleHelpers = await jiti.import("drizzle-orm");
3843
+ const schema = await jiti.import(join21(appRoot, "db/schema.ts"));
3844
+ const local = await openLocalD1(appRoot);
3616
3845
  try {
3617
- const bindings = env;
3618
- if (!bindings.DB) throw new Error("No DB binding found in wrangler.jsonc.");
3619
- const db = drizzle(bindings.DB, { schema });
3846
+ const db = drizzle(local.db, { schema });
3847
+ const insertMany = createInsertMany(local.db, drizzleHelpers);
3620
3848
  for (const file of files) {
3621
- const mod = await jiti.import(join19(appRoot, SEEDS_DIR, file));
3849
+ const mod = await jiti.import(join21(appRoot, SEEDS_DIR, file));
3622
3850
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3623
- log(`${pc15.cyan("seed")} ${file}`);
3624
- await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
3851
+ log2(`${pc17.cyan("seed")} ${file}`);
3852
+ await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
3625
3853
  }
3626
- log(pc15.green(`
3854
+ log2(pc17.green(`
3627
3855
  Ran ${files.length} seed(s) against the local database.`));
3628
3856
  } finally {
3629
- await dispose();
3857
+ await local.dispose();
3630
3858
  }
3631
3859
  }
3632
- function sampleValue(key, def, row) {
3860
+ function sampleValue(key, def) {
3861
+ const name = key.toLowerCase();
3633
3862
  switch (def.kind) {
3634
3863
  case "string":
3635
- if (def.format === "email") return `\`${kebabCase3(key).replace(/-?email$/, "") || "user"}\${i}@example.com\``;
3636
- if (def.format === "url") return `\`https://example.com/\${i}\``;
3637
- if (def.format === "tel") return `\`+1202555\${String(1000 + i).slice(-4)}\``;
3638
- if (def.format === "domain") return `\`example\${i}.com\``;
3639
- if (def.format === "country") return JSON.stringify(["US", "GB", "UG", "KE", "DE"][row % 5]);
3640
- if (def.format === "color") return JSON.stringify("#f2541d");
3641
- if (def.format === "slug") return `\`${kebabCase3(def.label ?? key)}-\${i}\``;
3642
- return `\`${def.label} \${i}\``;
3864
+ if (def.format === "email") return "fake.email()";
3865
+ if (def.format === "url") return "fake.url()";
3866
+ if (def.format === "tel") return "fake.phone()";
3867
+ if (def.format === "domain") return "fake.domain()";
3868
+ if (def.format === "country") return "fake.country()";
3869
+ if (def.format === "color") return "fake.color()";
3870
+ if (def.format === "slug") return "fake.slug()";
3871
+ if (name.includes("email")) return "fake.email()";
3872
+ if (name.includes("phone") || name.includes("mobile")) return "fake.phone()";
3873
+ if (name.includes("company")) return "fake.company()";
3874
+ if (name.includes("city")) return "fake.city()";
3875
+ if (name.includes("name")) return "fake.fullName()";
3876
+ return "fake.words(2)";
3643
3877
  case "text":
3644
- return `"Lorem ipsum dolor sit amet."`;
3878
+ return "fake.paragraph()";
3645
3879
  case "int":
3646
- return "i";
3880
+ return "fake.int(1, 1000)";
3647
3881
  case "float":
3648
- return "i * 1.5";
3882
+ return "fake.float(1, 1000)";
3649
3883
  case "boolean":
3650
- return "i % 2 === 0";
3884
+ return "fake.bool()";
3651
3885
  case "date":
3652
- return "`2026-01-0${i}`";
3886
+ return "fake.date()";
3653
3887
  case "datetime":
3654
- return "new Date().toISOString()";
3888
+ return "fake.datetime()";
3655
3889
  case "enum":
3656
- return JSON.stringify(def.options[row % def.options.length]);
3890
+ return `fake.pick(${JSON.stringify(def.options)})`;
3657
3891
  case "multiselect":
3658
- return JSON.stringify(def.options.slice(0, Math.max(1, def.minItems ?? 1)));
3892
+ return `fake.some(${JSON.stringify(def.options)}, ${Math.max(1, def.minItems ?? 1)})`;
3659
3893
  case "file":
3660
3894
  return void 0;
3661
3895
  case "belongsTo":
@@ -3666,9 +3900,10 @@ function renderSeed(resource) {
3666
3900
  if (!resource) {
3667
3901
  return `import { defineSeed } from "@flaredev/core";
3668
3902
 
3669
- export default defineSeed(async ({ db, log }) => {
3903
+ export default defineSeed(async ({ db, insertMany, fake, log }) => {
3670
3904
  // Insert rows with Drizzle, e.g.:
3671
3905
  // await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
3906
+ // Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
3672
3907
  log("nothing to seed yet");
3673
3908
  });
3674
3909
  `;
@@ -3677,58 +3912,369 @@ export default defineSeed(async ({ db, log }) => {
3677
3912
  const lines = [];
3678
3913
  const todos = [];
3679
3914
  for (const [key, def] of storedFields2(resource)) {
3680
- const value = sampleValue(key, def, 0);
3915
+ const value = sampleValue(key, def);
3681
3916
  if (value !== void 0) lines.push(` ${key}: ${value},`);
3682
3917
  else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
3683
3918
  }
3684
3919
  return `import { defineSeed } from "@flaredev/core";
3685
3920
  import { ${table} } from "@/db/schema";
3686
3921
 
3687
- export default defineSeed(async ({ db, log }) => {
3688
- const rows = [1, 2, 3].map((i) => ({
3922
+ /** How many to create. insertMany batches them, so hundreds of thousands are fine. */
3923
+ const COUNT = 50;
3924
+
3925
+ export default defineSeed(async ({ insertMany, fake, log }) => {
3926
+ const rows = await insertMany(${table}, COUNT, () => ({
3689
3927
  ${[...lines, ...todos].join("\n")}
3690
3928
  }));
3691
- await db.insert(${table}).values(rows);
3692
- log(\`inserted \${rows.length} ${resource.pluralLabel.toLowerCase()}\`);
3929
+ log(\`inserted \${rows.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
3693
3930
  });
3694
3931
  `;
3695
3932
  }
3696
3933
  async function makeSeed(rawName, options2 = {}) {
3697
- const log = options2.log ?? ((message) => console.log(message));
3934
+ const log2 = options2.log ?? ((message) => console.log(message));
3698
3935
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3699
- const name = kebabCase3(rawName);
3936
+ const name = kebabCase4(rawName);
3700
3937
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3701
3938
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3702
- const path = join19(appRoot, relative2);
3703
- if (existsSync20(path)) throw new Error(`${relative2} already exists.`);
3939
+ const path = join21(appRoot, relative2);
3940
+ if (existsSync21(path)) throw new Error(`${relative2} already exists.`);
3704
3941
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3705
3942
  let resource;
3706
3943
  if (options2.resource) {
3707
3944
  resource = resources.find((r) => r.name.toLowerCase() === options2.resource.toLowerCase());
3708
3945
  if (!resource) throw new Error(`No resource "${options2.resource}". Available: ${resources.map((r) => r.name).join(", ") || "none"}.`);
3709
3946
  } else {
3710
- resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
3947
+ resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
3711
3948
  }
3712
- mkdirSync10(join19(appRoot, SEEDS_DIR), { recursive: true });
3713
- writeFileSync13(path, renderSeed(resource));
3714
- log(`${pc15.green("create".padEnd(9))} ${relative2}`);
3715
- log(`
3716
- Run it with ${pc15.bold(`flare seed ${name}`)}.`);
3949
+ mkdirSync11(join21(appRoot, SEEDS_DIR), { recursive: true });
3950
+ writeFileSync14(path, renderSeed(resource));
3951
+ log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
3952
+ log2(`
3953
+ Run it with ${pc17.bold(`flare seed ${name}`)}.`);
3717
3954
  return relative2;
3718
3955
  }
3719
3956
 
3957
+ // src/commands/seed-resource.ts
3958
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
3959
+ import { tmpdir as tmpdir2 } from "os";
3960
+ import { join as join22 } from "path";
3961
+ import pc18 from "picocolors";
3962
+
3963
+ // src/seed/rows.ts
3964
+ import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
3965
+ var has = (key, ...words) => words.some((word) => key.includes(word));
3966
+ function stringValue(key, def, fake, resource, index) {
3967
+ switch (def.format) {
3968
+ case "email":
3969
+ return fake.email();
3970
+ case "url":
3971
+ return fake.url();
3972
+ case "tel":
3973
+ return fake.phone();
3974
+ case "domain":
3975
+ return fake.domain();
3976
+ case "country":
3977
+ return fake.country();
3978
+ case "color":
3979
+ return fake.color();
3980
+ case "slug":
3981
+ return fake.slug();
3982
+ }
3983
+ const name = key.toLowerCase();
3984
+ const organisation = has(resource.name.toLowerCase(), "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
3985
+ let value;
3986
+ if (has(name, "email")) value = fake.email();
3987
+ else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
3988
+ else if (has(name, "firstname", "givenname")) value = fake.firstName();
3989
+ else if (has(name, "lastname", "surname", "familyname")) value = fake.lastName();
3990
+ else if (has(name, "company", "organisation", "organization", "employer", "vendor", "supplier", "business")) value = fake.company();
3991
+ else if (has(name, "city", "town")) value = fake.city();
3992
+ else if (has(name, "country")) value = fake.country();
3993
+ else if (has(name, "address", "street")) value = `${fake.int(1, 240)} ${fake.words(1)} Street, ${fake.city()}`;
3994
+ else if (has(name, "website", "url", "link")) value = fake.url();
3995
+ else if (has(name, "slug", "handle", "username")) value = fake.slug();
3996
+ else if (has(name, "colour", "color")) value = fake.color();
3997
+ else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
3998
+ else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
3999
+ else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
4000
+ else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
4001
+ else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
4002
+ if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
4003
+ if (def.minLength && value.length < def.minLength) value = value.padEnd(def.minLength, "x");
4004
+ return value;
4005
+ }
4006
+ function numberValue(key, def, fake) {
4007
+ const name = key.toLowerCase();
4008
+ const money = has(name, "price", "amount", "total", "cost", "fee", "revenue", "salary", "balance");
4009
+ let min = def.min;
4010
+ let max = def.max;
4011
+ if (min === void 0 || max === void 0) {
4012
+ const [low, high] = money ? [5, 5e3] : has(name, "age") ? [18, 78] : has(name, "year") ? [2015, (/* @__PURE__ */ new Date()).getFullYear()] : has(name, "quantity", "qty", "stock", "seats", "items") ? [1, 250] : has(name, "percent", "rate", "score", "discount") ? [0, 100] : [1, 1e3];
4013
+ min = min ?? low;
4014
+ max = max ?? high;
4015
+ }
4016
+ return def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, money ? 2 : 2);
4017
+ }
4018
+ function fieldValue(key, def, fake, resource, parentIds, index) {
4019
+ const name = key.toLowerCase();
4020
+ if (!def.required && def.kind !== "belongsTo" && fake.bool(0.15)) return null;
4021
+ switch (def.kind) {
4022
+ case "string":
4023
+ return stringValue(key, def, fake, resource, index);
4024
+ case "text":
4025
+ return fake.paragraph(fake.int(1, 3));
4026
+ case "int":
4027
+ case "float":
4028
+ return numberValue(key, def, fake);
4029
+ case "boolean":
4030
+ return fake.bool(has(name, "active", "enabled", "published", "verified", "visible") ? 0.85 : 0.4);
4031
+ case "date":
4032
+ return has(name, "due", "expire", "end", "renew") ? fake.date(-90) : fake.date();
4033
+ case "datetime":
4034
+ return has(name, "due", "expire", "end", "renew") ? fake.datetime(-90) : fake.datetime();
4035
+ case "enum":
4036
+ return fake.pick(def.options);
4037
+ case "multiselect":
4038
+ return fake.some(def.options, Math.max(1, def.minItems ?? 1), def.maxItems ?? def.options.length);
4039
+ case "file":
4040
+ return def.required ? `seed/${resource.table}/${fake.count}.bin` : null;
4041
+ case "belongsTo": {
4042
+ const ids = parentIds.get(def.target) ?? [];
4043
+ if (ids.length === 0) {
4044
+ if (def.required) {
4045
+ throw new Error(`${resource.name}.${key} needs a ${def.target} to belong to, and there are none. Seed ${def.target} first.`);
4046
+ }
4047
+ return null;
4048
+ }
4049
+ return fake.pick(ids);
4050
+ }
4051
+ }
4052
+ }
4053
+ function uniquify(value, row) {
4054
+ const trailing = /(\d+)$/.exec(value)?.[1];
4055
+ if (trailing && Number(trailing) === row) return value;
4056
+ const at = value.indexOf("@");
4057
+ if (at > 0) return `${value.slice(0, at)}.${row}${value.slice(at)}`;
4058
+ if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
4059
+ return `${value} ${row}`;
4060
+ }
4061
+ function seedColumns(resource) {
4062
+ return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4063
+ }
4064
+ function* seedRows(resource, count, options2 = {}) {
4065
+ const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
4066
+ const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
4067
+ const fields = storedFields3(resource);
4068
+ const now = options2.now ?? Date.now();
4069
+ const start = options2.startIndex ?? 0;
4070
+ for (let i = 0; i < count; i++) {
4071
+ const number = start + i;
4072
+ const row = { id: fake.id() };
4073
+ for (const [key, def] of fields) {
4074
+ let value = fieldValue(key, def, fake, resource, parentIds, number);
4075
+ if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
4076
+ row[columnName2(key)] = value;
4077
+ }
4078
+ const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
4079
+ row.created_at = created;
4080
+ row.updated_at = created + fake.int(0, Math.min(now - created, 30 * 864e5));
4081
+ yield row;
4082
+ }
4083
+ }
4084
+
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
+ // src/commands/seed-resource.ts
4123
+ async function countRows(db, table) {
4124
+ const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
4125
+ return row?.total ?? 0;
4126
+ }
4127
+ var PARENT_SAMPLE = 5e3;
4128
+ function parseCount(value, fallback = 25) {
4129
+ if (value === void 0 || value === "") return fallback;
4130
+ if (typeof value === "number") return Math.floor(value);
4131
+ const text = String(value).trim().toLowerCase().replaceAll(",", "").replaceAll("_", "").replaceAll(" ", "");
4132
+ const match = /^(\d+(?:\.\d+)?)([km])?$/.exec(text);
4133
+ if (!match) throw new Error(`"${value}" is not a number of rows. Try 1000, 25k or 1m.`);
4134
+ const size = Number(match[1]) * (match[2] === "k" ? 1e3 : match[2] === "m" ? 1e6 : 1);
4135
+ if (size < 1) throw new Error("Ask for at least one row.");
4136
+ return Math.floor(size);
4137
+ }
4138
+ function pickResource(resources, name) {
4139
+ const wanted = name.toLowerCase();
4140
+ const match = resources.find((resource) => [resource.name.toLowerCase(), resource.slug, resource.table].includes(wanted));
4141
+ if (!match) {
4142
+ throw new Error(`No resource "${name}". Available: ${resources.map((resource) => resource.name).join(", ") || "none"}.`);
4143
+ }
4144
+ return match;
4145
+ }
4146
+ function pickDatabase(databases, wanted) {
4147
+ if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4148
+ if (wanted) {
4149
+ const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
4150
+ if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
4151
+ return match;
4152
+ }
4153
+ if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
4154
+ return databases[0];
4155
+ }
4156
+ async function parentsOf(resource, all, ids) {
4157
+ const targets = new Set(
4158
+ Object.values(resource.fields).filter((field) => field.kind === "belongsTo").map((field) => field.target)
4159
+ );
4160
+ const parents = /* @__PURE__ */ new Map();
4161
+ for (const target of targets) {
4162
+ const parent = all.find((candidate) => candidate.name === target);
4163
+ if (parent) parents.set(target, await ids(parent.table));
4164
+ }
4165
+ return parents;
4166
+ }
4167
+ async function seedResourceCommand(name, options2 = {}) {
4168
+ const progress = progressLine();
4169
+ const started = Date.now();
4170
+ try {
4171
+ const result = await seedResource(name, {
4172
+ ...options2,
4173
+ onProgress: (written, total) => {
4174
+ const elapsed = Date.now() - started;
4175
+ progress.update(` ${pc18.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc18.dim(formatRate(written, elapsed))}`);
4176
+ }
4177
+ });
4178
+ progress.done();
4179
+ const { rows, ms, where } = result;
4180
+ 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)})`)
4182
+ );
4183
+ return result;
4184
+ } catch (error) {
4185
+ progress.done();
4186
+ throw error;
4187
+ }
4188
+ }
4189
+ async function seedResource(name, options2 = {}) {
4190
+ const log2 = options2.log ?? ((message) => console.log(message));
4191
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4192
+ const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4193
+ const resource = pickResource(resources, name);
4194
+ const total = parseCount(options2.count);
4195
+ const where = options2.remote ? "remote" : "local";
4196
+ if (options2.remote && !options2.yes) {
4197
+ throw new Error(`Seeding the remote database writes ${total.toLocaleString()} rows to ${resource.table}. Re-run with --yes to confirm.`);
4198
+ }
4199
+ const columns = seedColumns(resource);
4200
+ const started = Date.now();
4201
+ let parents = /* @__PURE__ */ new Map();
4202
+ let startIndex = 0;
4203
+ const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
4204
+ if (options2.remote) {
4205
+ const db = pickDatabase(readD1Databases(appRoot), options2.database);
4206
+ const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4207
+ const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4208
+ const query = async (sql2) => {
4209
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--json", "--command", sql2], appRoot, { capture: true });
4210
+ if (result.code !== 0) throw new Error(`Query failed on the remote database:
4211
+ ${result.output.trim()}`);
4212
+ const start = result.output.indexOf("[");
4213
+ const parsed = JSON.parse(result.output.slice(start, result.output.lastIndexOf("]") + 1));
4214
+ return parsed[0]?.results ?? [];
4215
+ };
4216
+ parents = await parentsOf(resource, resources, async (table) => {
4217
+ const found = await query(`SELECT id FROM "${table}" LIMIT ${PARENT_SAMPLE}`);
4218
+ return found.map((row) => row.id);
4219
+ });
4220
+ if (!options2.truncate) {
4221
+ const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4222
+ startIndex = counted?.total ?? 0;
4223
+ }
4224
+ const dir = mkdtempSync2(join22(tmpdir2(), "flare-seed-"));
4225
+ const file = join22(dir, `${resource.table}.sql`);
4226
+ try {
4227
+ const statements = [];
4228
+ if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
4229
+ let written = 0;
4230
+ for (const statement of insertStatements(resource.table, columns, rows())) {
4231
+ statements.push(statement.sql);
4232
+ written += statement.rows;
4233
+ options2.onProgress?.(written, total);
4234
+ }
4235
+ writeFileSync15(file, statements.join("\n"));
4236
+ log2(`${pc18.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc18.bold(db.database_name)}`);
4237
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4238
+ if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4239
+ ${result.output.trim()}`);
4240
+ } finally {
4241
+ rmSync3(dir, { recursive: true, force: true });
4242
+ }
4243
+ return { resource, rows: total, ms: Date.now() - started, where };
4244
+ }
4245
+ const local = await openLocalD1(appRoot, options2.database);
4246
+ try {
4247
+ parents = await parentsOf(resource, resources, async (table) => {
4248
+ const found = await local.db.prepare(`SELECT id FROM "${table}" LIMIT ${PARENT_SAMPLE}`).all();
4249
+ return (found.results ?? []).map((row) => row.id);
4250
+ });
4251
+ if (options2.truncate) await local.db.exec(`DELETE FROM "${resource.table}";`);
4252
+ else startIndex = await countRows(local.db, resource.table);
4253
+ const sink = { exec: (sql2) => local.db.exec(sql2) };
4254
+ const written = await bulkInsert(sink, {
4255
+ table: resource.table,
4256
+ columns,
4257
+ rows: rows(),
4258
+ onProgress: (count) => options2.onProgress?.(count, total)
4259
+ });
4260
+ return { resource, rows: written, ms: Date.now() - started, where };
4261
+ } finally {
4262
+ await local.dispose();
4263
+ }
4264
+ }
4265
+
3720
4266
  // src/commands/billing-sync.ts
3721
4267
  import { createRequire as createRequire2 } from "module";
3722
- import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
3723
- import { tmpdir as tmpdir2 } from "os";
3724
- import { join as join20 } from "path";
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";
3725
4271
  import { pathToFileURL as pathToFileURL2 } from "url";
3726
- import { kebabCase as kebabCase4 } from "@flaredev/core";
3727
- import pc16 from "picocolors";
4272
+ import { kebabCase as kebabCase5 } from "@flaredev/core";
4273
+ import pc19 from "picocolors";
3728
4274
  function stripeKeys(appRoot) {
3729
4275
  const map = {};
3730
- const path = join20(appRoot, ".dev.vars");
3731
- if (existsSync21(path)) {
4276
+ const path = join23(appRoot, ".dev.vars");
4277
+ if (existsSync22(path)) {
3732
4278
  for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
3733
4279
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
3734
4280
  if (match) map[match[1]] = match[2].trim();
@@ -3749,7 +4295,7 @@ function planRows(catalog) {
3749
4295
  return ok;
3750
4296
  });
3751
4297
  const currencies = new Set(usable.map((price) => price.currency));
3752
- const base = product.metadata?.slug || kebabCase4(product.name);
4298
+ const base = product.metadata?.slug || kebabCase5(product.name);
3753
4299
  for (const price of usable) {
3754
4300
  const interval = price.recurring?.interval ?? null;
3755
4301
  const parts = [base];
@@ -3806,16 +4352,16 @@ async function syncPortalConfiguration(stripe, rows, appName) {
3806
4352
  return existing ? (await stripe.billingPortal.configurations.update(existing.id, settings)).id : (await stripe.billingPortal.configurations.create(settings)).id;
3807
4353
  }
3808
4354
  async function syncPlans(options2 = {}) {
3809
- const log = options2.log ?? ((message) => console.log(message));
4355
+ const log2 = options2.log ?? ((message) => console.log(message));
3810
4356
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3811
4357
  const key = stripeKeys(appRoot).STRIPE_SECRET_KEY;
3812
4358
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
3813
4359
  const db = readD1Databases(appRoot)[0];
3814
4360
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
3815
- const appRequire = createRequire2(join20(appRoot, "package.json"));
4361
+ const appRequire = createRequire2(join23(appRoot, "package.json"));
3816
4362
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
3817
4363
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
3818
- const appName = JSON.parse(readFileSync13(join20(appRoot, "package.json"), "utf8")).name ?? "app";
4364
+ const appName = JSON.parse(readFileSync13(join23(appRoot, "package.json"), "utf8")).name ?? "app";
3819
4365
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
3820
4366
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
3821
4367
  if (products.length === 0) {
@@ -3836,88 +4382,88 @@ async function syncPlans(options2 = {}) {
3836
4382
  ${existingResult.output.trim()}`);
3837
4383
  }
3838
4384
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
3839
- const dir = mkdtempSync2(join20(tmpdir2(), "flare-sync-"));
4385
+ const dir = mkdtempSync3(join23(tmpdir3(), "flare-sync-"));
3840
4386
  try {
3841
- const file = join20(dir, "sync-plans.sql");
3842
- writeFileSync14(file, renderSyncSql(rows));
4387
+ const file = join23(dir, "sync-plans.sql");
4388
+ writeFileSync16(file, renderSyncSql(rows));
3843
4389
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3844
4390
  if (result.code !== 0) throw new Error(`Could not write plans:
3845
4391
  ${result.output.trim()}`);
3846
4392
  } finally {
3847
- rmSync3(dir, { recursive: true, force: true });
4393
+ rmSync4(dir, { recursive: true, force: true });
3848
4394
  }
3849
4395
  let created = 0;
3850
4396
  for (const row of rows) {
3851
4397
  const isNew = !known.has(row.stripePriceId);
3852
4398
  if (isNew) created++;
3853
4399
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
3854
- log(`${(isNew ? pc16.green("create") : pc16.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc16.dim(price)}`);
4400
+ log2(`${(isNew ? pc19.green("create") : pc19.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc19.dim(price)}`);
3855
4401
  }
3856
- for (const name of skipped) log(pc16.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4402
+ for (const name of skipped) log2(pc19.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3857
4403
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
3858
- if (configuration) log(`${pc16.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
3859
- log(pc16.green(`
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(`
3860
4406
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
3861
4407
  return { created, updated: rows.length - created, skipped };
3862
4408
  }
3863
4409
 
3864
4410
  // src/commands/sync.ts
3865
- import { existsSync as existsSync22 } from "fs";
3866
- import { join as join21 } from "path";
3867
- import pc17 from "picocolors";
4411
+ import { existsSync as existsSync23 } from "fs";
4412
+ import { join as join24 } from "path";
4413
+ import pc20 from "picocolors";
3868
4414
  async function syncTypes(options2 = {}) {
3869
- const log = options2.log ?? ((message) => console.log(message));
4415
+ const log2 = options2.log ?? ((message) => console.log(message));
3870
4416
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3871
4417
  const all = await loadResources(appRoot);
3872
4418
  const policies = await loadPolicies(appRoot);
3873
- const tracksPolicies = policies.length > 0 || existsSync22(join21(appRoot, "policies/index.ts"));
4419
+ const tracksPolicies = policies.length > 0 || existsSync23(join24(appRoot, "policies/index.ts"));
3874
4420
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
3875
- if (!options2.check) ensureSupportFiles(appRoot, log);
4421
+ if (!options2.check) ensureSupportFiles(appRoot, log2);
3876
4422
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
3877
- logResults(results, log);
4423
+ logResults(results, log2);
3878
4424
  const drift = results.filter((r) => r.outcome === "skipped-drift");
3879
4425
  const unmarked = results.filter((r) => r.outcome === "skipped-unmarked");
3880
4426
  const changed = results.filter((r) => r.outcome === "create" || r.outcome === "update");
3881
4427
  const orphans = findOrphans(appRoot, all);
3882
4428
  if (drift.length) {
3883
- log(
4429
+ log2(
3884
4430
  `
3885
- ${pc17.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4431
+ ${pc20.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3886
4432
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
3887
4433
  );
3888
4434
  }
3889
4435
  if (unmarked.length) {
3890
- log(`
3891
- ${pc17.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4436
+ log2(`
4437
+ ${pc20.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3892
4438
  }
3893
4439
  if (orphans.length) {
3894
4440
  const resources = [...new Set(orphans.map((o) => o.resource))];
3895
- log(`
3896
- ${pc17.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
3897
- for (const orphan of orphans) log(` ${orphan.path}`);
3898
- log(
3899
- `If the resource was deleted, remove them with ${pc17.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4441
+ log2(`
4442
+ ${pc20.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4443
+ for (const orphan of orphans) log2(` ${orphan.path}`);
4444
+ 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.`
3900
4446
  );
3901
4447
  }
3902
4448
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
3903
4449
  if (tablesChanged && !options2.check) {
3904
- log(`
3905
- ${pc17.cyan("Tables changed.")} Create a migration with ${pc17.bold("flare gen migration <name> --from-schema")}, then ${pc17.bold("flare migrate")}.`);
4450
+ log2(`
4451
+ ${pc20.cyan("Tables changed.")} Create a migration with ${pc20.bold("flare gen migration <name> --from-schema")}, then ${pc20.bold("flare migrate")}.`);
3906
4452
  }
3907
4453
  const problems = drift.length + unmarked.length + orphans.length;
3908
4454
  if (options2.check) {
3909
4455
  const stale = changed.length;
3910
- if (stale === 0 && problems === 0) log(pc17.green("Generated files are in sync with the resource descriptors."));
3911
- else log(`
3912
- ${pc17.red("Out of sync:")} ${stale} file(s) would change. Run ${pc17.bold("flare sync-types")}.`);
4456
+ if (stale === 0 && problems === 0) log2(pc20.green("Generated files are in sync with the resource descriptors."));
4457
+ else log2(`
4458
+ ${pc20.red("Out of sync:")} ${stale} file(s) would change. Run ${pc20.bold("flare sync-types")}.`);
3913
4459
  return stale + problems > 0 ? 1 : 0;
3914
4460
  }
3915
- if (changed.length === 0 && problems === 0) log(pc17.green("Everything is in sync."));
4461
+ if (changed.length === 0 && problems === 0) log2(pc20.green("Everything is in sync."));
3916
4462
  return problems > 0 ? 1 : 0;
3917
4463
  }
3918
4464
 
3919
4465
  // src/commands/user-role.ts
3920
- import pc18 from "picocolors";
4466
+ import pc21 from "picocolors";
3921
4467
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
3922
4468
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
3923
4469
  function userRoleSql(email, role) {
@@ -3926,13 +4472,13 @@ function userRoleSql(email, role) {
3926
4472
  return `UPDATE "user" SET role = '${role}', updated_at = cast(unixepoch('subsecond') * 1000 as integer) WHERE lower(email) = lower('${email}') RETURNING email, role`;
3927
4473
  }
3928
4474
  async function setUserRole(email, role, options2 = {}) {
3929
- const log = options2.log ?? ((message) => console.log(message));
4475
+ const log2 = options2.log ?? ((message) => console.log(message));
3930
4476
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3931
4477
  const sql2 = userRoleSql(email, role);
3932
4478
  const roles = await listRoles(options2);
3933
4479
  if (roles && !roles.includes(role)) {
3934
- log(pc18.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3935
- log(pc18.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
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" : ""}`));
3936
4482
  return 1;
3937
4483
  }
3938
4484
  const db = readD1Databases(appRoot)[0];
@@ -3946,32 +4492,130 @@ ${result.output.trim()}`);
3946
4492
  const [first] = extractJson(result.output);
3947
4493
  const updated = first?.results ?? [];
3948
4494
  if (updated.length === 0) {
3949
- log(pc18.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4495
+ log2(pc21.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
3950
4496
  return 1;
3951
4497
  }
3952
- log(pc18.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4498
+ log2(pc21.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3953
4499
  return 0;
3954
4500
  }
3955
4501
 
4502
+ // src/utils/install.ts
4503
+ import { spawn as spawn5 } from "child_process";
4504
+ import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
4505
+ import { join as join25 } from "path";
4506
+ function expectedPackages(dir) {
4507
+ const npmLock = join25(dir, "package-lock.json");
4508
+ if (existsSync24(npmLock)) {
4509
+ try {
4510
+ const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
4511
+ return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4512
+ } catch {
4513
+ return 0;
4514
+ }
4515
+ }
4516
+ const pnpmLock = join25(dir, "pnpm-lock.yaml");
4517
+ if (existsSync24(pnpmLock)) {
4518
+ try {
4519
+ const text = readFileSync14(pnpmLock, "utf8");
4520
+ const packages = text.slice(text.indexOf("\npackages:"));
4521
+ return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4522
+ } catch {
4523
+ return 0;
4524
+ }
4525
+ }
4526
+ return 0;
4527
+ }
4528
+ function installedPackages(dir) {
4529
+ const root = join25(dir, "node_modules");
4530
+ if (!existsSync24(root)) return 0;
4531
+ let total = 0;
4532
+ try {
4533
+ for (const entry of readdirSync9(root, { withFileTypes: true })) {
4534
+ if (!entry.isDirectory() || entry.name === ".bin") continue;
4535
+ if (entry.name.startsWith("@")) {
4536
+ try {
4537
+ total += readdirSync9(join25(root, entry.name)).length;
4538
+ } catch {
4539
+ }
4540
+ } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
4541
+ total += 1;
4542
+ }
4543
+ }
4544
+ } catch {
4545
+ return total;
4546
+ }
4547
+ return total;
4548
+ }
4549
+ function install(packageManager, dir, onProgress) {
4550
+ const args = installArgs(packageManager);
4551
+ const started = Date.now();
4552
+ const expected = expectedPackages(dir);
4553
+ return new Promise((resolve6, reject) => {
4554
+ const child = process.platform === "win32" ? spawn5([packageManager, ...args].join(" "), { cwd: dir, shell: true, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] }) : spawn5(packageManager, args, { cwd: dir, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] });
4555
+ let output = "";
4556
+ child.stdout?.on("data", (chunk) => output += chunk);
4557
+ child.stderr?.on("data", (chunk) => output += chunk);
4558
+ const timer = onProgress ? setInterval(() => {
4559
+ const installed = installedPackages(dir);
4560
+ onProgress({
4561
+ installed,
4562
+ expected,
4563
+ percent: expected > 0 ? Math.min(99, Math.round(installed / expected * 100)) : void 0,
4564
+ elapsedMs: Date.now() - started
4565
+ });
4566
+ }, 500) : void 0;
4567
+ child.on("error", (error) => {
4568
+ clearInterval(timer);
4569
+ reject(error);
4570
+ });
4571
+ child.on("close", (code) => {
4572
+ clearInterval(timer);
4573
+ resolve6({ code: code ?? 1, output, ms: Date.now() - started });
4574
+ });
4575
+ });
4576
+ }
4577
+
3956
4578
  // src/cli.ts
3957
4579
  function createCli() {
3958
4580
  const cli = cac("flare");
3959
4581
  cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(async (dir, options2) => {
3960
- const install = !options2.skipInstall;
4582
+ const wanted = !options2.skipInstall;
3961
4583
  let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
3962
- if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers);
3963
- const result = createApp(dir, { pm: options2.pm, install, ...answers });
3964
- printNextSteps(result, install);
4584
+ if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
4585
+ const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
4586
+ let installed = false;
4587
+ if (wanted) {
4588
+ const spin = prompts3.spinner({ indicator: "timer" });
4589
+ spin.start(`Installing dependencies with ${result.packageManager}`);
4590
+ const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
4591
+ spin.message(
4592
+ percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
4593
+ );
4594
+ });
4595
+ installed = outcome.code === 0;
4596
+ if (installed) {
4597
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc22.dim(`(${formatDuration(outcome.ms)})`)}`);
4598
+ } else {
4599
+ spin.error(`${result.packageManager} install failed`);
4600
+ process.stderr.write(outcome.output);
4601
+ process.exitCode = 1;
4602
+ }
4603
+ }
4604
+ printNextSteps(result, installed);
3965
4605
  });
3966
- cli.command("gen <generator> [name]", "Generate code. Generators: resource, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
3967
- async (generator, name, options2) => {
4606
+ cli.command("gen <generator> [name] [second]", "Generate code. Generators: resource, endpoint, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
4607
+ async (generator, name, second, options2) => {
3968
4608
  if (generator === "billing")
3969
4609
  return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
3970
4610
  if (generator === "security") return genSecurity({ force: options2.force, skipMigration: options2.skipMigration });
3971
4611
  if (["resource", "migration", "policy"].includes(generator) && !name) {
3972
4612
  throw new Error(`flare gen ${generator} needs a name, e.g. flare gen ${generator} ${generator === "migration" ? "add_phone_to_contacts" : "Contact"}`);
3973
4613
  }
3974
- if (generator === "resource") return genResource(name, { fields: options2.fields, force: options2.force });
4614
+ if (generator === "resource") return genResource(name, { fields: options2.fields, force: options2.force, group: options2.group, icon: options2.icon });
4615
+ if (generator === "endpoint") {
4616
+ if (!name || !second) throw new Error("flare gen endpoint needs a resource and a name, e.g. flare gen endpoint Order publish");
4617
+ return genEndpoint(name, second, { method: options2.method, action: options2.action, record: options2.record });
4618
+ }
3975
4619
  if (generator === "migration") return genMigration(name, { fromSchema: options2.fromSchema });
3976
4620
  if (generator === "policy") return genPolicy(name, { roles: options2.roles, deleteRoles: options2.deleteRoles, force: options2.force });
3977
4621
  throw new Error(`Unknown generator "${generator}". Available: resource, migration, policy, billing, security.`);
@@ -3996,6 +4640,9 @@ function createCli() {
3996
4640
  cli.command("seed [...names]", "Run seed files from seeds/ against the local D1 database").example("flare seed # every seed, in file-name order").example("flare seed contacts # just seeds/contacts.seed.ts").action(async (names) => {
3997
4641
  await runSeeds({ names });
3998
4642
  });
4643
+ 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
+ await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
4645
+ });
3999
4646
  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) => {
4000
4647
  await makeSeed(name, options2);
4001
4648
  });
@@ -4022,7 +4669,7 @@ function createCli() {
4022
4669
  cli.command(`${name} [...args]`, spec.description).allowUnknownOptions();
4023
4670
  }
4024
4671
  cli.help();
4025
- cli.version(FLARE_VERSION2);
4672
+ cli.version(FLARE_VERSION3);
4026
4673
  return cli;
4027
4674
  }
4028
4675
 
@@ -4041,6 +4688,6 @@ try {
4041
4688
  }
4042
4689
  }
4043
4690
  } catch (error) {
4044
- console.error(pc19.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4691
+ console.error(pc23.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4045
4692
  process.exitCode = 1;
4046
4693
  }