@flaredev/cli 0.2.0 → 0.3.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 (64) hide show
  1. package/bin/flare.js +9 -1
  2. package/dist/index.js +751 -280
  3. package/package.json +2 -2
  4. package/templates/app/app/admin/page.tsx +9 -61
  5. package/templates/app/app/dashboard/account/page.tsx +6 -4
  6. package/templates/app/app/{admin → dashboard}/actions.ts +109 -11
  7. package/templates/app/app/dashboard/import-actions.ts +91 -0
  8. package/templates/app/app/dashboard/layout.tsx +42 -39
  9. package/templates/app/app/dashboard/observability/page.tsx +174 -0
  10. package/templates/app/app/dashboard/page.tsx +106 -25
  11. package/templates/app/app/verify-email/page.tsx +33 -0
  12. package/templates/app/components/account/security-settings.tsx +6 -9
  13. package/templates/app/components/auth/otp-input.tsx +95 -0
  14. package/templates/app/components/auth/password-field.tsx +116 -0
  15. package/templates/app/components/auth/password-forms.tsx +76 -79
  16. package/templates/app/components/auth/sign-in-flow.tsx +306 -315
  17. package/templates/app/components/auth/sign-up-form.tsx +93 -101
  18. package/templates/app/components/auth/two-factor-form.tsx +139 -124
  19. package/templates/app/components/auth/use-resend.ts +44 -0
  20. package/templates/app/components/auth/verify-email-form.tsx +135 -0
  21. package/templates/app/components/{admin/admin-header.tsx → dashboard/dashboard-header.tsx} +78 -78
  22. package/templates/app/components/{admin/resource-nav.tsx → dashboard/dashboard-sidebar.tsx} +166 -120
  23. package/templates/app/components/dashboard/export-button.tsx +41 -0
  24. package/templates/app/components/{admin → dashboard}/fields/file-field.tsx +302 -302
  25. package/templates/app/components/dashboard/import-dialog.tsx +487 -0
  26. package/templates/app/components/dashboard/page-header.tsx +57 -0
  27. package/templates/app/components/dashboard/resource-form-dialog.tsx +80 -0
  28. package/templates/app/components/{admin → dashboard}/resource-form-page.tsx +61 -55
  29. package/templates/app/components/{admin → dashboard}/resource-form.tsx +165 -139
  30. package/templates/app/components/dashboard/resource-stats.tsx +53 -0
  31. package/templates/app/components/{admin → dashboard}/resource-table.tsx +331 -281
  32. package/templates/app/components/{admin → dashboard}/row-actions.tsx +144 -111
  33. package/templates/app/components/dashboard/stat-card.tsx +69 -0
  34. package/templates/app/components/dashboard/table-selection.tsx +185 -0
  35. package/templates/app/components/dashboard/traffic-chart.tsx +32 -0
  36. package/templates/app/db/flare-schema.ts +33 -1
  37. package/templates/app/lib/analytics.ts +121 -0
  38. package/templates/app/lib/audit.ts +80 -0
  39. package/templates/app/lib/auth-config.ts +38 -33
  40. package/templates/app/lib/auth.ts +34 -0
  41. package/templates/app/lib/cache.ts +83 -83
  42. package/templates/app/lib/csv.ts +24 -0
  43. package/templates/app/lib/dashboard-nav.ts +15 -0
  44. package/templates/app/lib/{admin.ts → dashboard.ts} +138 -103
  45. package/templates/app/lib/password-rules.ts +159 -0
  46. package/templates/app/lib/site.ts +56 -47
  47. package/templates/app/migrations/0000_init.sql +15 -3
  48. package/templates/app/migrations/meta/0000_snapshot.json +97 -1
  49. package/templates/app/migrations/meta/_journal.json +1 -1
  50. package/templates/app/worker-configuration.d.ts +15764 -0
  51. package/templates/locks/package-lock.json +9189 -0
  52. package/templates/locks/pnpm-lock.yaml +6347 -0
  53. package/templates/app/app/admin/layout.tsx +0 -32
  54. package/templates/app/components/dashboard/dashboard-nav.tsx +0 -35
  55. package/templates/app/lib/admin-nav.ts +0 -15
  56. /package/templates/app/components/{admin → dashboard}/fields/country-field.tsx +0 -0
  57. /package/templates/app/components/{admin → dashboard}/fields/date-field.tsx +0 -0
  58. /package/templates/app/components/{admin → dashboard}/fields/field-widget.tsx +0 -0
  59. /package/templates/app/components/{admin → dashboard}/fields/multi-select-field.tsx +0 -0
  60. /package/templates/app/components/{admin → dashboard}/fields/phone-field.tsx +0 -0
  61. /package/templates/app/components/{admin → dashboard}/fields/relation-field.tsx +0 -0
  62. /package/templates/app/components/{admin → dashboard}/query.ts +0 -0
  63. /package/templates/app/components/{admin → dashboard}/resource-icon.tsx +0 -0
  64. /package/templates/app/components/{admin → dashboard}/resource-table-toolbar.tsx +0 -0
package/dist/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  // src/index.ts
2
- import pc19 from "picocolors";
2
+ import pc21 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 * as prompts3 from "@clack/prompts";
7
+ import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
7
8
 
8
9
  // src/commands/create.ts
9
10
  import { randomBytes } from "crypto";
10
- import { appendFileSync, existsSync as existsSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
11
+ import { appendFileSync, copyFileSync, existsSync as existsSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
11
12
  import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
13
+ import * as prompts from "@clack/prompts";
12
14
  import pc from "picocolors";
13
15
  import { FLARE_VERSION } from "@flaredev/core";
14
16
 
@@ -86,6 +88,11 @@ export const authConfig = {
86
88
  social: ${JSON.stringify(providers).replace(/","/g, '", "')} as SocialProvider[],
87
89
  /** Refuse password sign-in until the email address is verified. */
88
90
  requireEmailVerification: false,
91
+ /**
92
+ * Check new passwords against Have I Been Pwned's breach list. Only the first five
93
+ * characters of the password's hash are sent, and an outage lets the password through.
94
+ */
95
+ checkBreachedPasswords: true,
89
96
  };
90
97
 
91
98
  export type SocialProvider = "google" | "github" | "apple" | "microsoft";
@@ -178,6 +185,43 @@ var APP_DEV_DEPENDENCIES = {
178
185
  wrangler: "^4.132.0"
179
186
  };
180
187
 
188
+ // src/terminal.ts
189
+ var isTTY = () => Boolean(process.stdout.isTTY);
190
+ function formatDuration(ms) {
191
+ const seconds = Math.round(ms / 1e3);
192
+ if (seconds < 60) return `${seconds}s`;
193
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
194
+ return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
195
+ }
196
+ var formatCount = (value) => value.toLocaleString();
197
+ function formatRate(count, ms, unit = "rows") {
198
+ if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
199
+ return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
200
+ }
201
+ function progressLine(everyMs = 120) {
202
+ if (!isTTY()) return { update: () => {
203
+ }, done: () => {
204
+ } };
205
+ let last = 0;
206
+ let width = 0;
207
+ const write = (message) => {
208
+ process.stdout.write(`\r${message.padEnd(width)}`);
209
+ width = Math.max(width, message.length);
210
+ };
211
+ return {
212
+ update(message) {
213
+ const now = Date.now();
214
+ if (now - last < everyMs) return;
215
+ last = now;
216
+ write(message);
217
+ },
218
+ done() {
219
+ if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
220
+ width = 0;
221
+ }
222
+ };
223
+ }
224
+
181
225
  // src/utils/fs.ts
182
226
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
183
227
  import { dirname, join, resolve } from "path";
@@ -185,16 +229,19 @@ import { fileURLToPath } from "url";
185
229
  var RENAMES = { _gitignore: ".gitignore" };
186
230
  function copyTemplate(from, to, tokens) {
187
231
  mkdirSync(to, { recursive: true });
232
+ let files = 0;
188
233
  for (const entry of readdirSync(from)) {
189
234
  const src = join(from, entry);
190
235
  const dest = join(to, RENAMES[entry] ?? entry);
191
236
  if (statSync(src).isDirectory()) {
192
- copyTemplate(src, dest, tokens);
237
+ files += copyTemplate(src, dest, tokens);
193
238
  continue;
194
239
  }
195
240
  const content = readFileSync(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
196
241
  writeFileSync(dest, content);
242
+ files++;
197
243
  }
244
+ return files;
198
245
  }
199
246
  function writeJson(path, value) {
200
247
  mkdirSync(dirname(path), { recursive: true });
@@ -275,7 +322,7 @@ function createApp(target, options2 = {}) {
275
322
  const theme = parseTheme(options2.theme);
276
323
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
277
324
  const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
278
- copyTemplate(join2(templatesDir, "app"), dir, {
325
+ const files = copyTemplate(join2(templatesDir, "app"), dir, {
279
326
  APP_NAME: name,
280
327
  COMPAT_DATE: compatibilityDate,
281
328
  PM: packageManager,
@@ -310,42 +357,40 @@ ${devVarsEntries(authProviders)}`
310
357
  if (packageManager === "pnpm" && !inWorkspace) {
311
358
  writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
312
359
  }
313
- const log = options2.log ?? ((message) => console.log(message));
360
+ const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
361
+ if (pinned) copyLockfile(packageManager, dir);
362
+ const log2 = options2.log ?? ((message) => console.log(message));
363
+ log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
314
364
  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
365
  const started = Date.now();
319
- const code = run(packageManager, installArgs(packageManager), dir);
366
+ const code = (options2.installer ?? run)(packageManager, installArgs(packageManager), dir);
320
367
  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`);
368
+ if (!options2.installer) log2(`${pc.green("\u2714")} Installed dependencies ${pc.dim(`(${formatDuration(Date.now() - started)})`)}`);
328
369
  }
329
370
  return { dir, name, packageManager, inWorkspace };
330
371
  }
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`;
372
+ function copyLockfile(packageManager, dir) {
373
+ const file = packageManager === "npm" ? "package-lock.json" : packageManager === "pnpm" ? "pnpm-lock.yaml" : void 0;
374
+ if (!file) return false;
375
+ const source = join2(templatesDir, "locks", file);
376
+ if (!existsSync2(source)) return false;
377
+ copyFileSync(source, join2(dir, file));
378
+ return true;
334
379
  }
335
380
  function printNextSteps(result, installed) {
336
381
  const rel = relative(process.cwd(), result.dir) || ".";
337
382
  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/")}`);
383
+ const commands = [
384
+ [`cd ${rel}`, "your new app"],
385
+ ...installed ? [] : [[`${pm} install`, "dependencies first"]],
386
+ [`${pm} run dev`, "http://localhost:3000"],
387
+ ["npx flare gen resource Contact", "a table, API and admin screens"],
388
+ ["npx flare seed:resource Contact 1000", "fill it with sample rows"]
389
+ ];
390
+ const width = Math.max(...commands.map(([command]) => command.length)) + 2;
391
+ const steps = commands.map(([command, note2]) => `${pc.cyan(command.padEnd(width))}${pc.dim(note2)}`);
392
+ prompts.note(steps.join("\n"), "Next");
393
+ prompts.outro(`${pc.bold(result.name)} is ready ${pc.dim("\xB7")} ${pc.cyan("https://flare-docs.codetotech.com/start/quickstart/")}`);
349
394
  }
350
395
 
351
396
  // src/commands/gen-billing.ts
@@ -1394,12 +1439,12 @@ function onPath() {
1394
1439
  const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1395
1440
  return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
1396
1441
  }
1397
- async function download(log) {
1442
+ async function download(log2) {
1398
1443
  const asset = releaseAsset();
1399
1444
  const dir = cacheDir();
1400
1445
  mkdirSync3(dir, { recursive: true });
1401
1446
  const target = join4(dir, binName);
1402
- log(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1447
+ log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1403
1448
  const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1404
1449
  if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1405
1450
  const bytes = Buffer.from(await response.arrayBuffer());
@@ -1416,12 +1461,12 @@ async function download(log) {
1416
1461
  if (process.platform !== "win32") chmodSync(target, 493);
1417
1462
  return target;
1418
1463
  }
1419
- async function ensureCloudflared(log = console.log) {
1464
+ async function ensureCloudflared(log2 = console.log) {
1420
1465
  const found = onPath();
1421
1466
  if (found) return found;
1422
1467
  const cached = join4(cacheDir(), binName);
1423
1468
  if (existsSync5(cached)) return cached;
1424
- return download(log);
1469
+ return download(log2);
1425
1470
  }
1426
1471
  async function openTunnel(localUrl, options2 = {}) {
1427
1472
  const bin = await ensureCloudflared(options2.log);
@@ -1575,9 +1620,9 @@ async function runDeploy(args, context3) {
1575
1620
  if (plan.passthroughOnly) return context3.deploy(plan.forwarded);
1576
1621
  const { appRoot, wranglerBin } = context3;
1577
1622
  const shellEnv = context3.env ?? process.env;
1578
- const log = context3.log ?? ((message) => console.log(message));
1623
+ const log2 = context3.log ?? ((message) => console.log(message));
1579
1624
  const envArgs = plan.env ? ["--env", plan.env] : [];
1580
- const step = (message) => log(`
1625
+ const step = (message) => log2(`
1581
1626
  ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1582
1627
  const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1583
1628
  const migrateAfterDeploy = [];
@@ -1609,7 +1654,7 @@ ${info.output.trim()}`);
1609
1654
  const apiToken = shellEnv.FLARE_SECURITY_API_TOKEN;
1610
1655
  if (security && zoneId && apiToken) {
1611
1656
  step("Provisioning zone security rules");
1612
- await provisionZoneSecurity(appRoot, security, { zoneId, apiToken }, { log, fetch: context3.fetch });
1657
+ await provisionZoneSecurity(appRoot, security, { zoneId, apiToken }, { log: log2, fetch: context3.fetch });
1613
1658
  } else if (security) {
1614
1659
  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
1660
  }
@@ -1642,7 +1687,7 @@ ${put.output.trim()}`);
1642
1687
  const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1643
1688
  if (optional.length > 0) {
1644
1689
  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}` : ""}`);
1690
+ for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
1646
1691
  }
1647
1692
  }
1648
1693
  return 0;
@@ -1849,7 +1894,7 @@ function previousSnapshot(dir, migrationFile) {
1849
1894
  }
1850
1895
 
1851
1896
  // src/generator/plan.ts
1852
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1897
+ import { copyFileSync as copyFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1853
1898
  import { dirname as dirname5, join as join10 } from "path";
1854
1899
  import pc5 from "picocolors";
1855
1900
 
@@ -2078,16 +2123,24 @@ var adminImports = (entry) => [
2078
2123
  function renderAdminListPage(entry) {
2079
2124
  const local = resourceLocal(entry.stem);
2080
2125
  return [
2081
- `import { ResourceTable } from "@/components/admin/resource-table";`,
2082
- `import type { SearchParams } from "@/components/admin/query";`,
2126
+ `import { PageHeader } from "@/components/dashboard/page-header";`,
2127
+ `import { ResourceStats } from "@/components/dashboard/resource-stats";`,
2128
+ `import { ResourceTable } from "@/components/dashboard/resource-table";`,
2129
+ `import type { SearchParams } from "@/components/dashboard/query";`,
2083
2130
  ...adminImports(entry),
2084
2131
  "",
2132
+ `export const metadata = { title: ${local}.pluralLabel };`,
2133
+ "",
2085
2134
  `export default async function ${entry.resource.name}ListPage({ searchParams }: { searchParams: Promise<SearchParams> }) {`,
2086
2135
  " return (",
2087
- ` <div className="flex flex-col gap-6">`,
2088
- ` <h1 className="text-2xl font-semibold">{${local}.pluralLabel}</h1>`,
2136
+ " <>",
2137
+ " <PageHeader",
2138
+ ` title={${local}.pluralLabel}`,
2139
+ ` crumbs={[{ label: "Dashboard", href: "/dashboard" }, { label: ${local}.pluralLabel }]}`,
2140
+ " />",
2141
+ ` <ResourceStats resource={${local}} />`,
2089
2142
  ` <ResourceTable resource={${local}} searchParams={await searchParams} />`,
2090
- " </div>",
2143
+ " </>",
2091
2144
  " );",
2092
2145
  "}",
2093
2146
  ""
@@ -2095,7 +2148,7 @@ function renderAdminListPage(entry) {
2095
2148
  }
2096
2149
  function renderAdminNewPage(entry) {
2097
2150
  return [
2098
- `import { ResourceFormPage } from "@/components/admin/resource-form-page";`,
2151
+ `import { ResourceFormPage } from "@/components/dashboard/resource-form-page";`,
2099
2152
  ...adminImports(entry),
2100
2153
  "",
2101
2154
  `export default function New${entry.resource.name}Page() {`,
@@ -2106,7 +2159,7 @@ function renderAdminNewPage(entry) {
2106
2159
  }
2107
2160
  function renderAdminEditPage(entry) {
2108
2161
  return [
2109
- `import { ResourceFormPage } from "@/components/admin/resource-form-page";`,
2162
+ `import { ResourceFormPage } from "@/components/dashboard/resource-form-page";`,
2110
2163
  ...adminImports(entry),
2111
2164
  "",
2112
2165
  `export default async function Edit${entry.resource.name}Page({ params }: { params: Promise<{ id: string }> }) {`,
@@ -2174,9 +2227,9 @@ function resourceFiles(entry, all) {
2174
2227
  { path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry) },
2175
2228
  { path: `resources/${stem}.client.ts`, content: renderClient(entry) },
2176
2229
  { 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) }
2230
+ { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2231
+ { path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2232
+ { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
2180
2233
  ];
2181
2234
  }
2182
2235
 
@@ -2236,7 +2289,7 @@ function findOrphans(appRoot, all) {
2236
2289
  for (const dir of list2("app/api", () => true)) {
2237
2290
  for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
2238
2291
  }
2239
- for (const dir of list2("app/admin", () => true)) {
2292
+ for (const dir of list2("app/dashboard", () => true)) {
2240
2293
  for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2241
2294
  if (existsSync11(join10(appRoot, file))) candidates.push(file);
2242
2295
  }
@@ -2248,20 +2301,20 @@ function findOrphans(appRoot, all) {
2248
2301
  }
2249
2302
  return orphans;
2250
2303
  }
2251
- function ensureSupportFiles(appRoot, log) {
2304
+ function ensureSupportFiles(appRoot, log2) {
2252
2305
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2253
2306
  const target = join10(appRoot, path);
2254
2307
  if (existsSync11(target)) continue;
2255
2308
  mkdirSync4(dirname5(target), { recursive: true });
2256
- copyFileSync(join10(templatesDir, "app", path), target);
2257
- log(`${pc5.green("create".padEnd(9))} ${path}`);
2309
+ copyFileSync2(join10(templatesDir, "app", path), target);
2310
+ log2(`${pc5.green("create".padEnd(9))} ${path}`);
2258
2311
  }
2259
2312
  }
2260
- function logResults(results, log, options2 = {}) {
2313
+ function logResults(results, log2, options2 = {}) {
2261
2314
  for (const { path, outcome } of results) {
2262
2315
  if (outcome === "identical" && !options2.showIdentical) continue;
2263
2316
  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}`);
2317
+ log2(`${label.padEnd(18)} ${path}`);
2265
2318
  }
2266
2319
  }
2267
2320
 
@@ -2296,20 +2349,20 @@ function addStripeDependency(appRoot) {
2296
2349
  writeJson(path, pkg);
2297
2350
  return true;
2298
2351
  }
2299
- function writeBillingResource(appRoot, name, force, log) {
2352
+ function writeBillingResource(appRoot, name, force, log2) {
2300
2353
  const relative2 = descriptorPath(name);
2301
2354
  const path = join11(appRoot, relative2);
2302
2355
  if (!existsSync12(path)) {
2303
2356
  mkdirSync5(dirname6(path), { recursive: true });
2304
2357
  writeFileSync6(path, renderBillingDescriptor(name));
2305
- log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2358
+ log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2306
2359
  return;
2307
2360
  }
2308
2361
  const parts = splitMarkers(readFileSync8(path, "utf8"));
2309
2362
  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
2363
  const merged = mergeBillingFields(name, parts.block);
2311
2364
  if (!merged) {
2312
- log(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2365
+ log2(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2313
2366
  return;
2314
2367
  }
2315
2368
  try {
@@ -2320,22 +2373,22 @@ function writeBillingResource(appRoot, name, force, log) {
2320
2373
  }
2321
2374
  throw error;
2322
2375
  }
2323
- log(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2376
+ log2(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2324
2377
  }
2325
2378
  async function genBilling(options2 = {}) {
2326
- const log = options2.log ?? ((message) => console.log(message));
2379
+ const log2 = options2.log ?? ((message) => console.log(message));
2327
2380
  validateBillingOptions(options2.provider, options2.mode);
2328
2381
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2329
- for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log);
2382
+ for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2330
2383
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2331
2384
  const relative2 = policyPath(name);
2332
2385
  if (existsSync12(join11(appRoot, relative2))) {
2333
- log(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2386
+ log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2334
2387
  continue;
2335
2388
  }
2336
2389
  mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2337
2390
  writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2338
- log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2391
+ log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2339
2392
  }
2340
2393
  const files = [];
2341
2394
  const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
@@ -2343,7 +2396,7 @@ async function genBilling(options2 = {}) {
2343
2396
  const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2344
2397
  files.push({ path, status: "missing", outcome });
2345
2398
  const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2346
- log(`${label.padEnd(18)} ${path}`);
2399
+ log2(`${label.padEnd(18)} ${path}`);
2347
2400
  }
2348
2401
  for (const path of RETIRED_FILES) {
2349
2402
  const absolute = join11(appRoot, path);
@@ -2352,15 +2405,15 @@ async function genBilling(options2 = {}) {
2352
2405
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2353
2406
  if (untouched) {
2354
2407
  unlinkSync(absolute);
2355
- log(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2408
+ log2(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2356
2409
  } else {
2357
- log(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2410
+ log2(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2358
2411
  }
2359
2412
  }
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`);
2413
+ if (appendVars(appRoot, ".dev.vars.example")) log2(`${pc6.green("append".padEnd(9))} .dev.vars.example`);
2414
+ if (appendVars(appRoot, ".dev.vars")) log2(`${pc6.green("append".padEnd(9))} .dev.vars`);
2362
2415
  const addedStripe = addStripeDependency(appRoot);
2363
- if (addedStripe) log(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2416
+ if (addedStripe) log2(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2364
2417
  if (!options2.skipInstall) {
2365
2418
  const pm = detectPackageManager();
2366
2419
  const code = run(pm, ["install"], appRoot);
@@ -2368,16 +2421,16 @@ async function genBilling(options2 = {}) {
2368
2421
  run(pm, ["run", "cf-typegen"], appRoot);
2369
2422
  }
2370
2423
  const all = await loadResources(appRoot);
2371
- ensureSupportFiles(appRoot, log);
2372
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log);
2424
+ ensureSupportFiles(appRoot, log2);
2425
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2373
2426
  let migrations = [];
2374
2427
  if (!options2.skipMigration) {
2375
2428
  const result = await generateSchemaMigration(appRoot, "billing");
2376
2429
  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}`));
2430
+ for (const file of migrations) log2(`${pc6.green("create".padEnd(9))} ${file}`);
2431
+ for (const repair of result.repairs) log2(pc6.dim(`repaired ${repair}`));
2379
2432
  }
2380
- log(
2433
+ log2(
2381
2434
  [
2382
2435
  "",
2383
2436
  `Next:`,
@@ -2519,7 +2572,7 @@ var renderSecurityActions = () => `"use server";
2519
2572
  import { revalidatePath } from "next/cache";
2520
2573
  import { can } from "@flaredev/core";
2521
2574
  import { isIp } from "@flaredev/core/security";
2522
- import { adminSession, policyFor } from "@/lib/admin";
2575
+ import { dashboardSession, policyFor } from "@/lib/dashboard";
2523
2576
  import { logSecurityEvent, security } from "@/lib/security";
2524
2577
 
2525
2578
  /** On failure the submitted values come back, so the form (which React resets) keeps them. */
@@ -2528,7 +2581,7 @@ export type BanResult = { ok: true } | { ok: false; error: string; ip?: string;
2528
2581
  const DURATIONS: Record<string, number | null> = { "1h": 3600, "24h": 86_400, "7d": 604_800, permanent: null };
2529
2582
 
2530
2583
  async function adminOnly(): Promise<{ email: string } | { error: string }> {
2531
- const { session, role } = await adminSession();
2584
+ const { session, role } = await dashboardSession();
2532
2585
  if (!session) return { error: "Sign in required." };
2533
2586
  if (!can(policyFor("SecurityEvent"), role, "create")) return { error: "Only admins can ban or unban." };
2534
2587
  return { email: session.user.email };
@@ -2549,8 +2602,8 @@ export async function banAction(_previous: BanResult | null, form: FormData): Pr
2549
2602
  } catch (error) {
2550
2603
  return { ok: false, error: \`The ban didn't reach every layer: \${error instanceof Error ? error.message : String(error)}\`, ip, reason };
2551
2604
  }
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");
2605
+ await logSecurityEvent({ kind: "manual-ban", severity: "medium", ip, path: "/dashboard/security", userAgent: null, count: 0, banned: true, detail: \`\${reason} (by \${admin.email})\` });
2606
+ revalidatePath("/dashboard/security");
2554
2607
  return { ok: true };
2555
2608
  }
2556
2609
 
@@ -2563,8 +2616,8 @@ export async function unbanAction(ip: string): Promise<BanResult> {
2563
2616
  } catch (error) {
2564
2617
  return { ok: false, error: \`The unban didn't reach every layer: \${error instanceof Error ? error.message : String(error)}\` };
2565
2618
  }
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");
2619
+ await logSecurityEvent({ kind: "manual-unban", severity: "low", ip, path: "/dashboard/security", userAgent: null, count: 0, banned: false, detail: \`by \${admin.email}\` });
2620
+ revalidatePath("/dashboard/security");
2568
2621
  return { ok: true };
2569
2622
  }
2570
2623
  `;
@@ -2657,7 +2710,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
2657
2710
  import { Separator } from "@/components/ui/separator";
2658
2711
  import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty";
2659
2712
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
2660
- import { adminStore, policyFor, requireAdmin } from "@/lib/admin";
2713
+ import { dashboardStore, policyFor, requireDashboard } from "@/lib/dashboard";
2661
2714
  import { security, zoneClient } from "@/lib/security";
2662
2715
  import { BanForm, UnbanButton } from "./ban-controls";
2663
2716
 
@@ -2666,7 +2719,7 @@ const when = (value: string | number | Date | null) =>
2666
2719
  value ? new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)) : "Until unbanned";
2667
2720
 
2668
2721
  export default async function SecurityPage() {
2669
- const { role } = await requireAdmin("/admin/security");
2722
+ const { role } = await requireDashboard("/dashboard/security");
2670
2723
  if (!can(policyFor("SecurityEvent"), role, "read")) {
2671
2724
  return (
2672
2725
  <Alert>
@@ -2679,7 +2732,7 @@ export default async function SecurityPage() {
2679
2732
 
2680
2733
  const [bans, events, edge] = await Promise.all([
2681
2734
  security().bans(),
2682
- adminStore("SecurityEvent").list(new URLSearchParams({ sort: "-createdAt", perPage: "20" })),
2735
+ dashboardStore("SecurityEvent").list(new URLSearchParams({ sort: "-createdAt", perPage: "20" })),
2683
2736
  zone
2684
2737
  ? Promise.all([zone.bans(), zone.firewallEvents(new Date(Date.now() - 24 * 3600 * 1000))]).then(
2685
2738
  ([zoneBans, firewall]) => ({ ok: true as const, zoneBans, firewall }),
@@ -2790,7 +2843,7 @@ export default async function SecurityPage() {
2790
2843
  <CardHeader>
2791
2844
  <CardTitle>Recent events</CardTitle>
2792
2845
  <CardDescription>
2793
- Detectors that tripped, and manual bans. <Link href="/admin/security-events" className="underline underline-offset-4">All events</Link>
2846
+ Detectors that tripped, and manual bans. <Link href="/dashboard/security-events" className="underline underline-offset-4">All events</Link>
2794
2847
  </CardDescription>
2795
2848
  </CardHeader>
2796
2849
  <CardContent>
@@ -2900,43 +2953,43 @@ function addSecurityBindings(source) {
2900
2953
  }
2901
2954
  return { text, added };
2902
2955
  }
2903
- var renderAdminNavBlock = () => `export interface AdminLink {
2956
+ var renderAdminNavBlock = () => `export interface DashboardLink {
2904
2957
  label: string;
2905
2958
  href: string;
2906
- /** A resource icon name, e.g. "shield" (see components/admin/resource-icon.tsx). */
2959
+ /** A resource icon name, e.g. "shield" (see components/dashboard/resource-icon.tsx). */
2907
2960
  icon: string;
2908
2961
  }
2909
2962
 
2910
- export const generatedAdminLinks: AdminLink[] = [{ label: "Security", href: "/admin/security", icon: "shield" }];
2963
+ export const generatedDashboardLinks: DashboardLink[] = [{ label: "Security", href: "/dashboard/security", icon: "shield" }];
2911
2964
  `;
2912
2965
 
2913
2966
  // src/commands/gen-security.ts
2914
2967
  function securityFiles(appName) {
2915
2968
  return [
2916
2969
  { 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() }
2970
+ { path: "app/dashboard/security/page.tsx", content: renderSecurityPage() },
2971
+ { path: "app/dashboard/security/actions.ts", content: renderSecurityActions() },
2972
+ { path: "app/dashboard/security/ban-controls.tsx", content: renderBanControls() }
2920
2973
  ];
2921
2974
  }
2922
2975
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2923
- function writeOnce(appRoot, relative2, content, log) {
2976
+ function writeOnce(appRoot, relative2, content, log2) {
2924
2977
  const path = join12(appRoot, relative2);
2925
2978
  if (existsSync13(path)) {
2926
- log(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2979
+ log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2927
2980
  return;
2928
2981
  }
2929
2982
  mkdirSync6(dirname7(path), { recursive: true });
2930
2983
  writeFileSync7(path, content);
2931
- log(`${pc7.green("create".padEnd(9))} ${relative2}`);
2984
+ log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
2932
2985
  }
2933
2986
  async function genSecurity(options2 = {}) {
2934
- const log = options2.log ?? ((message) => console.log(message));
2987
+ const log2 = options2.log ?? ((message) => console.log(message));
2935
2988
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2936
2989
  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);
2990
+ writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
2991
+ writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
2992
+ writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
2940
2993
  const files = [];
2941
2994
  const write = (path, content, header) => {
2942
2995
  const absolute = join12(appRoot, path);
@@ -2947,12 +3000,12 @@ async function genSecurity(options2 = {}) {
2947
3000
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
2948
3001
  files.push({ path, status: "missing", outcome });
2949
3002
  const label = outcome === "create" ? pc7.green("create") : outcome === "update" ? pc7.yellow("update") : pc7.dim("identical");
2950
- log(`${label.padEnd(18)} ${path}`);
3003
+ log2(`${label.padEnd(18)} ${path}`);
2951
3004
  };
2952
3005
  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.`));
3006
+ const navPath = join12(appRoot, "lib/dashboard-nav.ts");
3007
+ if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
3008
+ else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
2956
3009
  const wranglerPath = join12(appRoot, "wrangler.jsonc");
2957
3010
  let bindings = [];
2958
3011
  if (existsSync13(wranglerPath)) {
@@ -2960,27 +3013,27 @@ async function genSecurity(options2 = {}) {
2960
3013
  bindings = added;
2961
3014
  if (added.length) {
2962
3015
  writeFileSync7(wranglerPath, text);
2963
- for (const line of added) log(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
3016
+ for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2964
3017
  }
2965
3018
  } else {
2966
- log(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
3019
+ log2(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2967
3020
  }
2968
3021
  const all = await loadResources(appRoot);
2969
- ensureSupportFiles(appRoot, log);
2970
- logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log);
3022
+ ensureSupportFiles(appRoot, log2);
3023
+ logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
2971
3024
  let migrations = [];
2972
3025
  if (!options2.skipMigration) {
2973
3026
  const result = await generateSchemaMigration(appRoot, "security");
2974
3027
  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}`));
3028
+ for (const file of migrations) log2(`${pc7.green("create".padEnd(9))} ${file}`);
3029
+ for (const repair of result.repairs) log2(pc7.dim(`repaired ${repair}`));
2977
3030
  }
2978
- log(
3031
+ log2(
2979
3032
  [
2980
3033
  "",
2981
3034
  `Next:`,
2982
3035
  ` 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.`,
3036
+ ` 2. ${pc7.bold("flare dev")}, then open ${pc7.bold("/dashboard/security")}. Tune detectors in security.config.ts.`,
2984
3037
  ` 3. Optional zone layer: deploy with FLARE_SECURITY_ZONE_ID and FLARE_SECURITY_API_TOKEN set (a token scoped to that`,
2985
3038
  ` one zone with Zone WAF Edit, Firewall Services Edit, Analytics Read, Zone Read). ${pc7.bold("flare deploy")} pushes the`,
2986
3039
  ` zone rules and uploads both as secrets.`
@@ -2994,7 +3047,7 @@ import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSy
2994
3047
  import { join as join13 } from "path";
2995
3048
  import pc8 from "picocolors";
2996
3049
  var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
2997
- function setTheme(name, cwd = process.cwd(), log = console.log) {
3050
+ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
2998
3051
  const appRoot = findAppRoot(cwd);
2999
3052
  const path = join13(appRoot, "lib/site.ts");
3000
3053
  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 +3055,50 @@ function setTheme(name, cwd = process.cwd(), log = console.log) {
3002
3055
  const current = THEME_LINE.exec(source)?.[1];
3003
3056
  if (!name) {
3004
3057
  for (const [theme2, description] of Object.entries(THEMES)) {
3005
- log(`${theme2 === current ? pc8.green("\u25CF") : " "} ${theme2.padEnd(8)} ${pc8.dim(description)}`);
3058
+ log2(`${theme2 === current ? pc8.green("\u25CF") : " "} ${theme2.padEnd(8)} ${pc8.dim(description)}`);
3006
3059
  }
3007
- log(pc8.dim("\nSwitch with `flare theme <name>`. FLARE_THEME in .env overrides it for a build."));
3060
+ log2(pc8.dim("\nSwitch with `flare theme <name>`. FLARE_THEME in .env overrides it for a build."));
3008
3061
  return;
3009
3062
  }
3010
3063
  const theme = parseTheme(name);
3011
3064
  if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
3012
3065
  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.`);
3066
+ log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
3014
3067
  }
3015
3068
 
3016
3069
  // src/commands/create-prompts.ts
3017
- import * as prompts from "@clack/prompts";
3070
+ import * as prompts2 from "@clack/prompts";
3071
+ import pc9 from "picocolors";
3072
+ import { FLARE_VERSION as FLARE_VERSION2 } from "@flaredev/core";
3018
3073
  var canPrompt = (yes) => !yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
3074
+ var AUTH_HINTS = {
3075
+ "magic-link": "a sign-in link by email",
3076
+ "email-otp": "a 6-digit sign-in code by email",
3077
+ passkeys: "Face ID, Touch ID, Windows Hello",
3078
+ "2fa-app": "authenticator app, with backup codes",
3079
+ "2fa-email": "a code by email as the second step"
3080
+ };
3081
+ var AUTH_LABELS = {
3082
+ "magic-link": "Magic links",
3083
+ "email-otp": "Email codes",
3084
+ passkeys: "Passkeys",
3085
+ "2fa-app": "Two-factor: authenticator app",
3086
+ "2fa-email": "Two-factor: email code"
3087
+ };
3019
3088
  function stopIfCancelled(value) {
3020
- if (prompts.isCancel(value)) {
3021
- prompts.cancel("Cancelled: nothing was created.");
3089
+ if (prompts2.isCancel(value)) {
3090
+ prompts2.cancel("Cancelled: nothing was created.");
3022
3091
  process.exit(1);
3023
3092
  }
3024
3093
  return value;
3025
3094
  }
3026
- async function askCreateQuestions(given) {
3027
- prompts.intro("Let's set up your Flare app");
3095
+ async function askCreateQuestions(given, appName) {
3096
+ prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
3028
3097
  const answers = { ...given };
3029
3098
  if (given.theme === void 0) {
3030
3099
  answers.theme = stopIfCancelled(
3031
- await prompts.select({
3032
- message: "Pick a look (change it later in lib/site.ts or with FLARE_THEME)",
3100
+ await prompts2.select({
3101
+ message: "Pick a look",
3033
3102
  initialValue: "default",
3034
3103
  options: Object.entries(THEMES).map(([value, hint]) => ({ value, label: value, hint }))
3035
3104
  })
@@ -3037,19 +3106,23 @@ async function askCreateQuestions(given) {
3037
3106
  }
3038
3107
  if (given.auth === void 0) {
3039
3108
  const methods = stopIfCancelled(
3040
- await prompts.multiselect({
3041
- message: "How can people sign in? Email and password is always on. (space to toggle)",
3109
+ await prompts2.multiselect({
3110
+ message: `How can people sign in? ${pc9.dim("Email and password is always on")}`,
3042
3111
  initialValues: DEFAULT_AUTH_METHODS,
3043
3112
  required: false,
3044
- options: Object.entries(AUTH_METHODS).map(([value, label]) => ({ value, label }))
3113
+ options: Object.keys(AUTH_METHODS).map((value) => ({
3114
+ value,
3115
+ label: AUTH_LABELS[value],
3116
+ hint: AUTH_HINTS[value]
3117
+ }))
3045
3118
  })
3046
3119
  );
3047
3120
  answers.auth = methods.length ? methods.join(",") : "none";
3048
3121
  }
3049
3122
  if (given.authProviders === void 0) {
3050
3123
  const providers = stopIfCancelled(
3051
- await prompts.multiselect({
3052
- message: "Social sign-in? Each needs its own credentials later. (space to toggle, enter to skip)",
3124
+ await prompts2.multiselect({
3125
+ message: `Social sign-in? ${pc9.dim("each needs its own credentials later")}`,
3053
3126
  initialValues: [],
3054
3127
  required: false,
3055
3128
  options: Object.entries(AUTH_PROVIDERS).map(([value, { label }]) => ({ value, label }))
@@ -3057,7 +3130,6 @@ async function askCreateQuestions(given) {
3057
3130
  );
3058
3131
  answers.authProviders = providers.join(",");
3059
3132
  }
3060
- prompts.outro("Creating your app");
3061
3133
  return answers;
3062
3134
  }
3063
3135
 
@@ -3065,8 +3137,8 @@ async function askCreateQuestions(given) {
3065
3137
  import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
3066
3138
  import { dirname as dirname8, join as join14 } from "path";
3067
3139
  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) {
3140
+ import pc10 from "picocolors";
3141
+ function validateFields(name, fields, others, log2) {
3070
3142
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
3071
3143
  for (const field of fields) {
3072
3144
  if (field.kind === "belongsTo" && !known.has(field.target)) {
@@ -3076,11 +3148,11 @@ function validateFields(name, fields, others, log) {
3076
3148
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
3077
3149
  const graph = relationGraph2([...others, candidate]);
3078
3150
  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).`));
3151
+ log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
3080
3152
  }
3081
3153
  }
3082
3154
  async function genResource(rawName, options2) {
3083
- const log = options2.log ?? ((message) => console.log(message));
3155
+ const log2 = options2.log ?? ((message) => console.log(message));
3084
3156
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3085
3157
  const name = resourceName(rawName);
3086
3158
  const relativePath = descriptorPath(name);
@@ -3093,15 +3165,15 @@ async function genResource(rawName, options2) {
3093
3165
  const others = existing.filter(({ resource }) => resource.name !== name).map(({ resource }) => resource);
3094
3166
  if (options2.fields) {
3095
3167
  const fields = parseFields(options2.fields);
3096
- validateFields(name, fields, others, log);
3168
+ validateFields(name, fields, others, log2);
3097
3169
  if (!exists) {
3098
3170
  mkdirSync7(dirname8(path), { recursive: true });
3099
3171
  writeFileSync9(path, renderDescriptor(name, fields));
3100
- log(`${pc9.green("create".padEnd(9))} ${relativePath}`);
3172
+ log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
3101
3173
  } else {
3102
3174
  try {
3103
3175
  const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
3104
- log(`${(outcome === "identical" ? pc9.dim : pc9.yellow)(outcome.padEnd(9))} ${relativePath}`);
3176
+ log2(`${(outcome === "identical" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relativePath}`);
3105
3177
  } catch (error) {
3106
3178
  if (error instanceof DriftError) {
3107
3179
  throw new Error(
@@ -3117,9 +3189,9 @@ async function genResource(rawName, options2) {
3117
3189
  try {
3118
3190
  const all = await loadResources(appRoot);
3119
3191
  table = all.find(({ resource }) => resource.name === name).resource.table;
3120
- ensureSupportFiles(appRoot, log);
3192
+ ensureSupportFiles(appRoot, log2);
3121
3193
  written = applyPlan(appRoot, planFiles(all), { force: options2.force });
3122
- logResults(written, log);
3194
+ logResults(written, log2);
3123
3195
  } catch (error) {
3124
3196
  const hint = exists ? "" : `
3125
3197
  (${relativePath} was written; fix it and run \`flare sync-types\`.)`;
@@ -3127,18 +3199,18 @@ async function genResource(rawName, options2) {
3127
3199
  }
3128
3200
  const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
3129
3201
  if (skipped.length) {
3130
- log(pc9.yellow(`
3202
+ log2(pc10.yellow(`
3131
3203
  Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
3132
3204
  }
3133
3205
  let migrations = [];
3134
3206
  if (!options2.skipMigration) {
3135
3207
  const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
3136
3208
  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."));
3209
+ for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
3210
+ for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
3211
+ if (migrations.length) log2(`
3212
+ Next: ${pc10.bold("flare migrate")} to apply it locally.`);
3213
+ else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
3142
3214
  }
3143
3215
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
3144
3216
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
@@ -3148,24 +3220,24 @@ Next: ${pc9.bold("flare migrate")} to apply it locally.`);
3148
3220
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
3149
3221
  import { basename as basename2, join as join15 } from "path";
3150
3222
  import { snakeCase } from "@flaredev/core";
3151
- import pc10 from "picocolors";
3223
+ import pc11 from "picocolors";
3152
3224
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
3153
3225
  -- Write SQL that undoes the up migration, e.g.:
3154
3226
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
3155
3227
  -- While this file has no statements, rollback of ${up} is refused.
3156
3228
  `;
3157
3229
  async function genMigration(rawName, options2 = {}) {
3158
- const log = options2.log ?? ((message) => console.log(message));
3230
+ const log2 = options2.log ?? ((message) => console.log(message));
3159
3231
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3160
3232
  const name = snakeCase(rawName);
3161
3233
  if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
3162
3234
  const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
3163
3235
  if (files.length === 0) {
3164
- log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
3236
+ log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
3165
3237
  return [];
3166
3238
  }
3167
- for (const file of files) log(`${pc10.green("create".padEnd(9))} ${file}`);
3168
- for (const repair of repairs) log(pc10.dim(`repaired ${repair}`));
3239
+ for (const file of files) log2(`${pc11.green("create".padEnd(9))} ${file}`);
3240
+ for (const repair of repairs) log2(pc11.dim(`repaired ${repair}`));
3169
3241
  if (!options2.fromSchema) {
3170
3242
  const downDir = join15(appRoot, "migrations", "down");
3171
3243
  mkdirSync8(downDir, { recursive: true });
@@ -3173,13 +3245,13 @@ async function genMigration(rawName, options2 = {}) {
3173
3245
  const down = join15(downDir, basename2(file));
3174
3246
  if (existsSync16(down)) continue;
3175
3247
  writeFileSync10(down, DOWN_TEMPLATE(basename2(file)));
3176
- log(`${pc10.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3248
+ log2(`${pc11.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
3177
3249
  }
3178
- log(`
3179
- Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc10.bold("flare migrate")}.`);
3250
+ log2(`
3251
+ Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc11.bold("flare migrate")}.`);
3180
3252
  } else {
3181
- log(`
3182
- Next: ${pc10.bold("flare migrate")}.`);
3253
+ log2(`
3254
+ Next: ${pc11.bold("flare migrate")}.`);
3183
3255
  }
3184
3256
  return files;
3185
3257
  }
@@ -3188,14 +3260,14 @@ Next: ${pc10.bold("flare migrate")}.`);
3188
3260
  import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
3189
3261
  import { dirname as dirname9, join as join16 } from "path";
3190
3262
  import { definePolicy } from "@flaredev/core";
3191
- import pc11 from "picocolors";
3263
+ import pc12 from "picocolors";
3192
3264
  function parseRoles(input, what = "--roles") {
3193
3265
  const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
3194
3266
  if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
3195
3267
  return [...new Set(roles)];
3196
3268
  }
3197
3269
  async function genPolicy(rawName, options2) {
3198
- const log = options2.log ?? ((message) => console.log(message));
3270
+ const log2 = options2.log ?? ((message) => console.log(message));
3199
3271
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3200
3272
  const name = resourceName(rawName);
3201
3273
  const resources = await loadResources(appRoot);
@@ -3218,11 +3290,11 @@ async function genPolicy(rawName, options2) {
3218
3290
  if (!exists) {
3219
3291
  mkdirSync9(dirname9(path), { recursive: true });
3220
3292
  writeFileSync11(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3221
- log(`${pc11.green("create".padEnd(9))} ${relative2}`);
3293
+ log2(`${pc12.green("create".padEnd(9))} ${relative2}`);
3222
3294
  } else {
3223
3295
  try {
3224
3296
  const outcome = writeGenerated(path, block, { force: options2.force });
3225
- log(`${(outcome === "identical" ? pc11.dim : pc11.yellow)(outcome.padEnd(9))} ${relative2}`);
3297
+ log2(`${(outcome === "identical" ? pc12.dim : pc12.yellow)(outcome.padEnd(9))} ${relative2}`);
3226
3298
  } catch (error) {
3227
3299
  if (error instanceof DriftError) {
3228
3300
  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,9 +3304,9 @@ async function genPolicy(rawName, options2) {
3232
3304
  }
3233
3305
  }
3234
3306
  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.")}`);
3307
+ logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
3308
+ log2(`
3309
+ ${pc12.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3238
3310
  return { name, path: relative2 };
3239
3311
  }
3240
3312
 
@@ -3242,7 +3314,7 @@ ${pc11.dim("Roles come from `flare role:add`; the API and the admin both enforce
3242
3314
  import { existsSync as existsSync18, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
3243
3315
  import { tmpdir } from "os";
3244
3316
  import { join as join17 } from "path";
3245
- import pc12 from "picocolors";
3317
+ import pc13 from "picocolors";
3246
3318
 
3247
3319
  // src/generator/down.ts
3248
3320
  var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
@@ -3358,11 +3430,11 @@ ${result.output.trim()}`);
3358
3430
  async function rollback(options2 = {}) {
3359
3431
  const steps = options2.steps ?? 1;
3360
3432
  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);
3433
+ const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
3362
3434
  const migrationsDir = join17(appRoot, db.migrations_dir ?? "migrations");
3363
3435
  const applied = await appliedMigrations(options2, steps);
3364
3436
  if (applied.length === 0) {
3365
- log("Nothing to roll back.");
3437
+ log2("Nothing to roll back.");
3366
3438
  return 0;
3367
3439
  }
3368
3440
  const plan = [];
@@ -3388,13 +3460,13 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3388
3460
  }
3389
3461
  plan.push({ name, sql: down.sql, source: "derived" });
3390
3462
  }
3391
- log(`${pc12.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3463
+ log2(`${pc13.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3392
3464
  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}`));
3465
+ log2(` ${step.name} ${pc13.dim(`(${step.source})`)}`);
3466
+ for (const line of step.sql.trim().split("\n")) log2(pc13.dim(` ${line}`));
3395
3467
  }
3396
3468
  if (options2.remote && !options2.yes) {
3397
- log(pc12.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3469
+ log2(pc13.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3398
3470
  return 1;
3399
3471
  }
3400
3472
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
@@ -3409,7 +3481,7 @@ ${result.output.trim()}`);
3409
3481
  } finally {
3410
3482
  rmSync2(dir, { recursive: true, force: true });
3411
3483
  }
3412
- log(pc12.green(`
3484
+ log2(pc13.green(`
3413
3485
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3414
3486
  return 0;
3415
3487
  }
@@ -3418,7 +3490,7 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3418
3490
  import { existsSync as existsSync19, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3419
3491
  import { dirname as dirname10, join as join18 } from "path";
3420
3492
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3421
- import pc13 from "picocolors";
3493
+ import pc14 from "picocolors";
3422
3494
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
3423
3495
  function handWrittenReason(source, header, options2 = {}) {
3424
3496
  const parts = splitMarkers(source);
@@ -3436,7 +3508,7 @@ function handWrittenReason(source, header, options2 = {}) {
3436
3508
  }
3437
3509
  function removeEmptyDirs(appRoot, start) {
3438
3510
  let dir = dirname10(join18(appRoot, start));
3439
- const roots = [join18(appRoot, "app", "api"), join18(appRoot, "app", "admin")];
3511
+ const roots = [join18(appRoot, "app", "api"), join18(appRoot, "app", "dashboard")];
3440
3512
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3441
3513
  while (inside() && existsSync19(dir) && readdirSync7(dir).length === 0) {
3442
3514
  rmdirSync(dir);
@@ -3444,7 +3516,7 @@ function removeEmptyDirs(appRoot, start) {
3444
3516
  }
3445
3517
  }
3446
3518
  async function rmResource(rawName, options2 = {}) {
3447
- const log = options2.log ?? ((message) => console.log(message));
3519
+ const log2 = options2.log ?? ((message) => console.log(message));
3448
3520
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3449
3521
  const name = resourceName(rawName);
3450
3522
  const all = await loadResources(appRoot);
@@ -3499,32 +3571,32 @@ Move what you want to keep, or pass --force to delete anyway.`);
3499
3571
  for (const path of removed) {
3500
3572
  unlinkSync2(join18(appRoot, path));
3501
3573
  removeEmptyDirs(appRoot, path);
3502
- log(`${pc13.red("remove".padEnd(9))} ${path}`);
3574
+ log2(`${pc14.red("remove".padEnd(9))} ${path}`);
3503
3575
  }
3504
- logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
3576
+ logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
3505
3577
  const table = entry ? tableExport(entry.resource) : void 0;
3506
3578
  if (table && existsSync19(join18(appRoot, "seeds"))) {
3507
3579
  const seeds = readdirSync7(join18(appRoot, "seeds")).filter(
3508
3580
  (file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join18(appRoot, "seeds", file), "utf8"))
3509
3581
  );
3510
- if (seeds.length) log(pc13.yellow(`
3582
+ if (seeds.length) log2(pc14.yellow(`
3511
3583
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
3512
3584
  }
3513
3585
  let migrations = [];
3514
3586
  if (!options2.skipMigration && entry) {
3515
3587
  const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
3516
3588
  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.`);
3589
+ for (const file of migrations) log2(`${pc14.green("create".padEnd(9))} ${file}`);
3590
+ for (const repair of result.repairs) log2(pc14.dim(`repaired ${repair}`));
3591
+ if (migrations.length) log2(`
3592
+ Next: ${pc14.bold("flare migrate")} to drop the table locally.`);
3521
3593
  }
3522
3594
  return { removed, migrations };
3523
3595
  }
3524
3596
 
3525
3597
  // src/commands/role.ts
3526
3598
  import { humanize } from "@flaredev/core";
3527
- import pc14 from "picocolors";
3599
+ import pc15 from "picocolors";
3528
3600
  var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
3529
3601
  function roleInsertSql(name, label) {
3530
3602
  if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
@@ -3557,7 +3629,7 @@ ${result.output.trim()}`);
3557
3629
  return (first?.results ?? []).map((row) => row.name);
3558
3630
  }
3559
3631
  async function addRole(name, options2 = {}) {
3560
- const { appRoot, binding, wrangler, target, log } = context2(options2);
3632
+ const { appRoot, binding, wrangler, target, log: log2 } = context2(options2);
3561
3633
  const sql2 = roleInsertSql(name, options2.label ?? humanize(name));
3562
3634
  const result = await runWrangler(wrangler, ["d1", "execute", binding, ...target, "--json", "--command", sql2], appRoot, { capture: true });
3563
3635
  if (result.code !== 0) {
@@ -3569,23 +3641,136 @@ ${result.output.trim()}`);
3569
3641
  }
3570
3642
  const [first] = extractJson(result.output);
3571
3643
  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" : ""}`));
3644
+ log2(pc15.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3645
+ log2(pc15.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3574
3646
  return 0;
3575
3647
  }
3576
3648
 
3577
3649
  // src/commands/seed.ts
3578
3650
  import { existsSync as existsSync20, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync13 } from "fs";
3651
+ import { join as join20 } from "path";
3652
+ import { createFake, kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
3653
+ import { createJiti as createJiti4 } from "jiti";
3654
+ import pc16 from "picocolors";
3655
+
3656
+ // src/seed/bulk.ts
3657
+ var MAX_STATEMENT_BYTES = 8e4;
3658
+ var STATEMENTS_PER_CALL = 25;
3659
+ function sqlLiteral(value) {
3660
+ if (value === null || value === void 0) return "NULL";
3661
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
3662
+ if (typeof value === "boolean") return value ? "1" : "0";
3663
+ if (value instanceof Date) return String(value.getTime());
3664
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
3665
+ return `'${text.replaceAll("'", "''")}'`;
3666
+ }
3667
+ var tuple = (row, columns) => `(${columns.map((column) => sqlLiteral(row[column])).join(",")})`;
3668
+ function* insertStatements(table, columns, rows, maxBytes = MAX_STATEMENT_BYTES) {
3669
+ const head = `INSERT INTO "${table}" (${columns.map((column) => `"${column}"`).join(",")}) VALUES `;
3670
+ let values = [];
3671
+ let bytes = head.length;
3672
+ for (const row of rows) {
3673
+ const text = tuple(row, columns);
3674
+ if (values.length > 0 && bytes + text.length + 1 > maxBytes) {
3675
+ yield { sql: `${head}${values.join(",")};`, rows: values.length };
3676
+ values = [];
3677
+ bytes = head.length;
3678
+ }
3679
+ values.push(text);
3680
+ bytes += text.length + 1;
3681
+ }
3682
+ if (values.length > 0) yield { sql: `${head}${values.join(",")};`, rows: values.length };
3683
+ }
3684
+ async function bulkInsert(sink, options2) {
3685
+ const perCall = options2.statementsPerCall ?? STATEMENTS_PER_CALL;
3686
+ let written = 0;
3687
+ let batch = [];
3688
+ let batchRows = 0;
3689
+ const flush = async () => {
3690
+ if (batch.length === 0) return;
3691
+ await sink.exec(batch.join("\n"));
3692
+ written += batchRows;
3693
+ batch = [];
3694
+ batchRows = 0;
3695
+ options2.onProgress?.(written);
3696
+ };
3697
+ for (const statement of insertStatements(options2.table, options2.columns, options2.rows, options2.maxBytes)) {
3698
+ batch.push(statement.sql);
3699
+ batchRows += statement.rows;
3700
+ if (batch.length === perCall) await flush();
3701
+ }
3702
+ await flush();
3703
+ return written;
3704
+ }
3705
+
3706
+ // src/seed/insert-many.ts
3707
+ var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
3708
+ function createInsertMany(sink, drizzle, onProgress) {
3709
+ return async function insertMany(table, count, build) {
3710
+ const name = drizzle.getTableName(table);
3711
+ const columns = drizzle.getTableColumns(table);
3712
+ if (typeof count === "number" && !build) throw new Error(`insertMany(${name}, ${count}) needs a function to build each row.`);
3713
+ const source = typeof count === "number" ? (function* () {
3714
+ for (let i = 0; i < count; i++) yield build(i);
3715
+ })() : isIterable(count) ? count : [];
3716
+ const iterator = source[Symbol.iterator]();
3717
+ const first = iterator.next();
3718
+ if (first.done) return 0;
3719
+ const keys = new Set(Object.keys(first.value));
3720
+ for (const [key, column] of Object.entries(columns)) if (column.defaultFn) keys.add(key);
3721
+ const used = Object.keys(columns).filter((key) => keys.has(key));
3722
+ const unknownKeys = [...keys].filter((key) => !columns[key]);
3723
+ if (unknownKeys.length) throw new Error(`${name} has no column called ${unknownKeys.join(", ")}.`);
3724
+ const toRow = (value) => {
3725
+ const row = {};
3726
+ for (const key of used) {
3727
+ const column = columns[key];
3728
+ let raw = value[key];
3729
+ if (raw === void 0 && column.defaultFn) raw = column.defaultFn();
3730
+ row[column.name] = raw === void 0 || raw === null ? null : column.mapToDriverValue?.(raw) ?? raw;
3731
+ }
3732
+ return row;
3733
+ };
3734
+ const rows = (function* () {
3735
+ yield toRow(first.value);
3736
+ for (let next = iterator.next(); !next.done; next = iterator.next()) yield toRow(next.value);
3737
+ })();
3738
+ return bulkInsert(sink, {
3739
+ table: name,
3740
+ columns: used.map((key) => columns[key].name),
3741
+ rows,
3742
+ onProgress: onProgress && ((written) => onProgress(written, name))
3743
+ });
3744
+ };
3745
+ }
3746
+
3747
+ // src/seed/local-d1.ts
3579
3748
  import { createRequire } from "module";
3580
3749
  import { join as join19 } from "path";
3581
3750
  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
3751
  var LOCAL_STATE = ".wrangler/state/v3";
3752
+ async function openLocalD1(appRoot, binding) {
3753
+ process.env.WRANGLER_LOG ??= "error";
3754
+ const appRequire = createRequire(join19(appRoot, "package.json"));
3755
+ const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3756
+ const { env, dispose } = await wrangler.getPlatformProxy({
3757
+ configPath: join19(appRoot, "wrangler.jsonc"),
3758
+ persist: { path: join19(appRoot, LOCAL_STATE) }
3759
+ });
3760
+ const bindings = env;
3761
+ const name = binding ?? "DB";
3762
+ const db = bindings[name];
3763
+ if (!db) {
3764
+ const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
3765
+ throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
3766
+ }
3767
+ return { db, env: bindings, dispose };
3768
+ }
3769
+
3770
+ // src/commands/seed.ts
3771
+ var SEEDS_DIR = "seeds";
3587
3772
  function seedFiles(appRoot, names) {
3588
- const dir = join19(appRoot, SEEDS_DIR);
3773
+ const dir = join20(appRoot, SEEDS_DIR);
3589
3774
  const all = existsSync20(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3590
3775
  if (!names?.length) return all;
3591
3776
  return names.map((name) => {
@@ -3597,65 +3782,66 @@ function seedFiles(appRoot, names) {
3597
3782
  });
3598
3783
  }
3599
3784
  async function runSeeds(options2 = {}) {
3600
- const log = options2.log ?? ((message) => console.log(message));
3785
+ const log2 = options2.log ?? ((message) => console.log(message));
3601
3786
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3602
3787
  const files = seedFiles(appRoot, options2.names);
3603
3788
  if (files.length === 0) {
3604
- log(`No seeds found. Create one with ${pc15.bold("flare seed:make <name>")}.`);
3789
+ log2(`No seeds found. Create one with ${pc16.bold("flare seed:make <name>")}.`);
3605
3790
  return;
3606
3791
  }
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);
3792
+ const jiti = createJiti4(join20(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3610
3793
  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
- });
3794
+ const drizzleHelpers = await jiti.import("drizzle-orm");
3795
+ const schema = await jiti.import(join20(appRoot, "db/schema.ts"));
3796
+ const local = await openLocalD1(appRoot);
3616
3797
  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 });
3798
+ const db = drizzle(local.db, { schema });
3799
+ const insertMany = createInsertMany(local.db, drizzleHelpers);
3620
3800
  for (const file of files) {
3621
- const mod = await jiti.import(join19(appRoot, SEEDS_DIR, file));
3801
+ const mod = await jiti.import(join20(appRoot, SEEDS_DIR, file));
3622
3802
  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}`) });
3803
+ log2(`${pc16.cyan("seed")} ${file}`);
3804
+ await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
3625
3805
  }
3626
- log(pc15.green(`
3806
+ log2(pc16.green(`
3627
3807
  Ran ${files.length} seed(s) against the local database.`));
3628
3808
  } finally {
3629
- await dispose();
3809
+ await local.dispose();
3630
3810
  }
3631
3811
  }
3632
- function sampleValue(key, def, row) {
3812
+ function sampleValue(key, def) {
3813
+ const name = key.toLowerCase();
3633
3814
  switch (def.kind) {
3634
3815
  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}\``;
3816
+ if (def.format === "email") return "fake.email()";
3817
+ if (def.format === "url") return "fake.url()";
3818
+ if (def.format === "tel") return "fake.phone()";
3819
+ if (def.format === "domain") return "fake.domain()";
3820
+ if (def.format === "country") return "fake.country()";
3821
+ if (def.format === "color") return "fake.color()";
3822
+ if (def.format === "slug") return "fake.slug()";
3823
+ if (name.includes("email")) return "fake.email()";
3824
+ if (name.includes("phone") || name.includes("mobile")) return "fake.phone()";
3825
+ if (name.includes("company")) return "fake.company()";
3826
+ if (name.includes("city")) return "fake.city()";
3827
+ if (name.includes("name")) return "fake.fullName()";
3828
+ return "fake.words(2)";
3643
3829
  case "text":
3644
- return `"Lorem ipsum dolor sit amet."`;
3830
+ return "fake.paragraph()";
3645
3831
  case "int":
3646
- return "i";
3832
+ return "fake.int(1, 1000)";
3647
3833
  case "float":
3648
- return "i * 1.5";
3834
+ return "fake.float(1, 1000)";
3649
3835
  case "boolean":
3650
- return "i % 2 === 0";
3836
+ return "fake.bool()";
3651
3837
  case "date":
3652
- return "`2026-01-0${i}`";
3838
+ return "fake.date()";
3653
3839
  case "datetime":
3654
- return "new Date().toISOString()";
3840
+ return "fake.datetime()";
3655
3841
  case "enum":
3656
- return JSON.stringify(def.options[row % def.options.length]);
3842
+ return `fake.pick(${JSON.stringify(def.options)})`;
3657
3843
  case "multiselect":
3658
- return JSON.stringify(def.options.slice(0, Math.max(1, def.minItems ?? 1)));
3844
+ return `fake.some(${JSON.stringify(def.options)}, ${Math.max(1, def.minItems ?? 1)})`;
3659
3845
  case "file":
3660
3846
  return void 0;
3661
3847
  case "belongsTo":
@@ -3666,9 +3852,10 @@ function renderSeed(resource) {
3666
3852
  if (!resource) {
3667
3853
  return `import { defineSeed } from "@flaredev/core";
3668
3854
 
3669
- export default defineSeed(async ({ db, log }) => {
3855
+ export default defineSeed(async ({ db, insertMany, fake, log }) => {
3670
3856
  // Insert rows with Drizzle, e.g.:
3671
3857
  // await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
3858
+ // Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
3672
3859
  log("nothing to seed yet");
3673
3860
  });
3674
3861
  `;
@@ -3677,29 +3864,31 @@ export default defineSeed(async ({ db, log }) => {
3677
3864
  const lines = [];
3678
3865
  const todos = [];
3679
3866
  for (const [key, def] of storedFields2(resource)) {
3680
- const value = sampleValue(key, def, 0);
3867
+ const value = sampleValue(key, def);
3681
3868
  if (value !== void 0) lines.push(` ${key}: ${value},`);
3682
3869
  else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
3683
3870
  }
3684
3871
  return `import { defineSeed } from "@flaredev/core";
3685
3872
  import { ${table} } from "@/db/schema";
3686
3873
 
3687
- export default defineSeed(async ({ db, log }) => {
3688
- const rows = [1, 2, 3].map((i) => ({
3874
+ /** How many to create. insertMany batches them, so hundreds of thousands are fine. */
3875
+ const COUNT = 50;
3876
+
3877
+ export default defineSeed(async ({ insertMany, fake, log }) => {
3878
+ const rows = await insertMany(${table}, COUNT, () => ({
3689
3879
  ${[...lines, ...todos].join("\n")}
3690
3880
  }));
3691
- await db.insert(${table}).values(rows);
3692
- log(\`inserted \${rows.length} ${resource.pluralLabel.toLowerCase()}\`);
3881
+ log(\`inserted \${rows.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
3693
3882
  });
3694
3883
  `;
3695
3884
  }
3696
3885
  async function makeSeed(rawName, options2 = {}) {
3697
- const log = options2.log ?? ((message) => console.log(message));
3886
+ const log2 = options2.log ?? ((message) => console.log(message));
3698
3887
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3699
3888
  const name = kebabCase3(rawName);
3700
3889
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3701
3890
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3702
- const path = join19(appRoot, relative2);
3891
+ const path = join20(appRoot, relative2);
3703
3892
  if (existsSync20(path)) throw new Error(`${relative2} already exists.`);
3704
3893
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3705
3894
  let resource;
@@ -3709,25 +3898,285 @@ async function makeSeed(rawName, options2 = {}) {
3709
3898
  } else {
3710
3899
  resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
3711
3900
  }
3712
- mkdirSync10(join19(appRoot, SEEDS_DIR), { recursive: true });
3901
+ mkdirSync10(join20(appRoot, SEEDS_DIR), { recursive: true });
3713
3902
  writeFileSync13(path, renderSeed(resource));
3714
- log(`${pc15.green("create".padEnd(9))} ${relative2}`);
3715
- log(`
3716
- Run it with ${pc15.bold(`flare seed ${name}`)}.`);
3903
+ log2(`${pc16.green("create".padEnd(9))} ${relative2}`);
3904
+ log2(`
3905
+ Run it with ${pc16.bold(`flare seed ${name}`)}.`);
3717
3906
  return relative2;
3718
3907
  }
3719
3908
 
3909
+ // src/commands/seed-resource.ts
3910
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
3911
+ import { tmpdir as tmpdir2 } from "os";
3912
+ import { join as join21 } from "path";
3913
+ import pc17 from "picocolors";
3914
+
3915
+ // src/seed/rows.ts
3916
+ import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
3917
+ var has = (key, ...words) => words.some((word) => key.includes(word));
3918
+ function stringValue(key, def, fake, resource, index) {
3919
+ switch (def.format) {
3920
+ case "email":
3921
+ return fake.email();
3922
+ case "url":
3923
+ return fake.url();
3924
+ case "tel":
3925
+ return fake.phone();
3926
+ case "domain":
3927
+ return fake.domain();
3928
+ case "country":
3929
+ return fake.country();
3930
+ case "color":
3931
+ return fake.color();
3932
+ case "slug":
3933
+ return fake.slug();
3934
+ }
3935
+ const name = key.toLowerCase();
3936
+ const organisation = has(resource.name.toLowerCase(), "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
3937
+ let value;
3938
+ if (has(name, "email")) value = fake.email();
3939
+ else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
3940
+ else if (has(name, "firstname", "givenname")) value = fake.firstName();
3941
+ else if (has(name, "lastname", "surname", "familyname")) value = fake.lastName();
3942
+ else if (has(name, "company", "organisation", "organization", "employer", "vendor", "supplier", "business")) value = fake.company();
3943
+ else if (has(name, "city", "town")) value = fake.city();
3944
+ else if (has(name, "country")) value = fake.country();
3945
+ else if (has(name, "address", "street")) value = `${fake.int(1, 240)} ${fake.words(1)} Street, ${fake.city()}`;
3946
+ else if (has(name, "website", "url", "link")) value = fake.url();
3947
+ else if (has(name, "slug", "handle", "username")) value = fake.slug();
3948
+ else if (has(name, "colour", "color")) value = fake.color();
3949
+ else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
3950
+ else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
3951
+ else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
3952
+ else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
3953
+ else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
3954
+ if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
3955
+ if (def.minLength && value.length < def.minLength) value = value.padEnd(def.minLength, "x");
3956
+ return value;
3957
+ }
3958
+ function numberValue(key, def, fake) {
3959
+ const name = key.toLowerCase();
3960
+ const money = has(name, "price", "amount", "total", "cost", "fee", "revenue", "salary", "balance");
3961
+ let min = def.min;
3962
+ let max = def.max;
3963
+ if (min === void 0 || max === void 0) {
3964
+ 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];
3965
+ min = min ?? low;
3966
+ max = max ?? high;
3967
+ }
3968
+ return def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, money ? 2 : 2);
3969
+ }
3970
+ function fieldValue(key, def, fake, resource, parentIds, index) {
3971
+ const name = key.toLowerCase();
3972
+ if (!def.required && def.kind !== "belongsTo" && fake.bool(0.15)) return null;
3973
+ switch (def.kind) {
3974
+ case "string":
3975
+ return stringValue(key, def, fake, resource, index);
3976
+ case "text":
3977
+ return fake.paragraph(fake.int(1, 3));
3978
+ case "int":
3979
+ case "float":
3980
+ return numberValue(key, def, fake);
3981
+ case "boolean":
3982
+ return fake.bool(has(name, "active", "enabled", "published", "verified", "visible") ? 0.85 : 0.4);
3983
+ case "date":
3984
+ return has(name, "due", "expire", "end", "renew") ? fake.date(-90) : fake.date();
3985
+ case "datetime":
3986
+ return has(name, "due", "expire", "end", "renew") ? fake.datetime(-90) : fake.datetime();
3987
+ case "enum":
3988
+ return fake.pick(def.options);
3989
+ case "multiselect":
3990
+ return fake.some(def.options, Math.max(1, def.minItems ?? 1), def.maxItems ?? def.options.length);
3991
+ case "file":
3992
+ return def.required ? `seed/${resource.table}/${fake.count}.bin` : null;
3993
+ case "belongsTo": {
3994
+ const ids = parentIds.get(def.target) ?? [];
3995
+ if (ids.length === 0) {
3996
+ if (def.required) {
3997
+ throw new Error(`${resource.name}.${key} needs a ${def.target} to belong to, and there are none. Seed ${def.target} first.`);
3998
+ }
3999
+ return null;
4000
+ }
4001
+ return fake.pick(ids);
4002
+ }
4003
+ }
4004
+ }
4005
+ function uniquify(value, row) {
4006
+ const trailing = /(\d+)$/.exec(value)?.[1];
4007
+ if (trailing && Number(trailing) === row) return value;
4008
+ const at = value.indexOf("@");
4009
+ if (at > 0) return `${value.slice(0, at)}.${row}${value.slice(at)}`;
4010
+ if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
4011
+ return `${value} ${row}`;
4012
+ }
4013
+ function seedColumns(resource) {
4014
+ return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
4015
+ }
4016
+ function* seedRows(resource, count, options2 = {}) {
4017
+ const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
4018
+ const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
4019
+ const fields = storedFields3(resource);
4020
+ const now = options2.now ?? Date.now();
4021
+ for (let i = 0; i < count; i++) {
4022
+ const row = { id: fake.id() };
4023
+ for (const [key, def] of fields) {
4024
+ let value = fieldValue(key, def, fake, resource, parentIds, i);
4025
+ if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, i + 1);
4026
+ row[columnName2(key)] = value;
4027
+ }
4028
+ const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
4029
+ row.created_at = created;
4030
+ row.updated_at = created + fake.int(0, Math.min(now - created, 30 * 864e5));
4031
+ yield row;
4032
+ }
4033
+ }
4034
+
4035
+ // src/commands/seed-resource.ts
4036
+ var PARENT_SAMPLE = 5e3;
4037
+ function parseCount(value, fallback = 25) {
4038
+ if (value === void 0 || value === "") return fallback;
4039
+ if (typeof value === "number") return Math.floor(value);
4040
+ const text = String(value).trim().toLowerCase().replaceAll(",", "").replaceAll("_", "").replaceAll(" ", "");
4041
+ const match = /^(\d+(?:\.\d+)?)([km])?$/.exec(text);
4042
+ if (!match) throw new Error(`"${value}" is not a number of rows. Try 1000, 25k or 1m.`);
4043
+ const size = Number(match[1]) * (match[2] === "k" ? 1e3 : match[2] === "m" ? 1e6 : 1);
4044
+ if (size < 1) throw new Error("Ask for at least one row.");
4045
+ return Math.floor(size);
4046
+ }
4047
+ function pickResource(resources, name) {
4048
+ const wanted = name.toLowerCase();
4049
+ const match = resources.find((resource) => [resource.name.toLowerCase(), resource.slug, resource.table].includes(wanted));
4050
+ if (!match) {
4051
+ throw new Error(`No resource "${name}". Available: ${resources.map((resource) => resource.name).join(", ") || "none"}.`);
4052
+ }
4053
+ return match;
4054
+ }
4055
+ function pickDatabase(databases, wanted) {
4056
+ if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
4057
+ if (wanted) {
4058
+ const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
4059
+ if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
4060
+ return match;
4061
+ }
4062
+ if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
4063
+ return databases[0];
4064
+ }
4065
+ async function parentsOf(resource, all, ids) {
4066
+ const targets = new Set(
4067
+ Object.values(resource.fields).filter((field) => field.kind === "belongsTo").map((field) => field.target)
4068
+ );
4069
+ const parents = /* @__PURE__ */ new Map();
4070
+ for (const target of targets) {
4071
+ const parent = all.find((candidate) => candidate.name === target);
4072
+ if (parent) parents.set(target, await ids(parent.table));
4073
+ }
4074
+ return parents;
4075
+ }
4076
+ async function seedResourceCommand(name, options2 = {}) {
4077
+ const progress = progressLine();
4078
+ const started = Date.now();
4079
+ try {
4080
+ const result = await seedResource(name, {
4081
+ ...options2,
4082
+ onProgress: (written, total) => {
4083
+ const elapsed = Date.now() - started;
4084
+ progress.update(` ${pc17.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc17.dim(formatRate(written, elapsed))}`);
4085
+ }
4086
+ });
4087
+ progress.done();
4088
+ const { rows, ms, where } = result;
4089
+ console.log(
4090
+ `${pc17.green("\u2714")} Seeded ${pc17.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc17.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4091
+ );
4092
+ return result;
4093
+ } catch (error) {
4094
+ progress.done();
4095
+ throw error;
4096
+ }
4097
+ }
4098
+ async function seedResource(name, options2 = {}) {
4099
+ const log2 = options2.log ?? ((message) => console.log(message));
4100
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4101
+ const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4102
+ const resource = pickResource(resources, name);
4103
+ const total = parseCount(options2.count);
4104
+ const where = options2.remote ? "remote" : "local";
4105
+ if (options2.remote && !options2.yes) {
4106
+ throw new Error(`Seeding the remote database writes ${total.toLocaleString()} rows to ${resource.table}. Re-run with --yes to confirm.`);
4107
+ }
4108
+ const columns = seedColumns(resource);
4109
+ const started = Date.now();
4110
+ let parents = /* @__PURE__ */ new Map();
4111
+ const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents });
4112
+ if (options2.remote) {
4113
+ const db = pickDatabase(readD1Databases(appRoot), options2.database);
4114
+ const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4115
+ const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4116
+ const query = async (sql2) => {
4117
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--json", "--command", sql2], appRoot, { capture: true });
4118
+ if (result.code !== 0) throw new Error(`Query failed on the remote database:
4119
+ ${result.output.trim()}`);
4120
+ const start = result.output.indexOf("[");
4121
+ const parsed = JSON.parse(result.output.slice(start, result.output.lastIndexOf("]") + 1));
4122
+ return parsed[0]?.results ?? [];
4123
+ };
4124
+ parents = await parentsOf(resource, resources, async (table) => {
4125
+ const found = await query(`SELECT id FROM "${table}" LIMIT ${PARENT_SAMPLE}`);
4126
+ return found.map((row) => row.id);
4127
+ });
4128
+ const dir = mkdtempSync2(join21(tmpdir2(), "flare-seed-"));
4129
+ const file = join21(dir, `${resource.table}.sql`);
4130
+ try {
4131
+ const statements = [];
4132
+ if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
4133
+ let written = 0;
4134
+ for (const statement of insertStatements(resource.table, columns, rows())) {
4135
+ statements.push(statement.sql);
4136
+ written += statement.rows;
4137
+ options2.onProgress?.(written, total);
4138
+ }
4139
+ writeFileSync14(file, statements.join("\n"));
4140
+ log2(`${pc17.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc17.bold(db.database_name)}`);
4141
+ const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4142
+ if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4143
+ ${result.output.trim()}`);
4144
+ } finally {
4145
+ rmSync3(dir, { recursive: true, force: true });
4146
+ }
4147
+ return { resource, rows: total, ms: Date.now() - started, where };
4148
+ }
4149
+ const local = await openLocalD1(appRoot, options2.database);
4150
+ try {
4151
+ parents = await parentsOf(resource, resources, async (table) => {
4152
+ const found = await local.db.prepare(`SELECT id FROM "${table}" LIMIT ${PARENT_SAMPLE}`).all();
4153
+ return (found.results ?? []).map((row) => row.id);
4154
+ });
4155
+ if (options2.truncate) await local.db.exec(`DELETE FROM "${resource.table}";`);
4156
+ const sink = { exec: (sql2) => local.db.exec(sql2) };
4157
+ const written = await bulkInsert(sink, {
4158
+ table: resource.table,
4159
+ columns,
4160
+ rows: rows(),
4161
+ onProgress: (count) => options2.onProgress?.(count, total)
4162
+ });
4163
+ return { resource, rows: written, ms: Date.now() - started, where };
4164
+ } finally {
4165
+ await local.dispose();
4166
+ }
4167
+ }
4168
+
3720
4169
  // src/commands/billing-sync.ts
3721
4170
  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";
4171
+ import { existsSync as existsSync21, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
4172
+ import { tmpdir as tmpdir3 } from "os";
4173
+ import { join as join22 } from "path";
3725
4174
  import { pathToFileURL as pathToFileURL2 } from "url";
3726
4175
  import { kebabCase as kebabCase4 } from "@flaredev/core";
3727
- import pc16 from "picocolors";
4176
+ import pc18 from "picocolors";
3728
4177
  function stripeKeys(appRoot) {
3729
4178
  const map = {};
3730
- const path = join20(appRoot, ".dev.vars");
4179
+ const path = join22(appRoot, ".dev.vars");
3731
4180
  if (existsSync21(path)) {
3732
4181
  for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
3733
4182
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
@@ -3806,16 +4255,16 @@ async function syncPortalConfiguration(stripe, rows, appName) {
3806
4255
  return existing ? (await stripe.billingPortal.configurations.update(existing.id, settings)).id : (await stripe.billingPortal.configurations.create(settings)).id;
3807
4256
  }
3808
4257
  async function syncPlans(options2 = {}) {
3809
- const log = options2.log ?? ((message) => console.log(message));
4258
+ const log2 = options2.log ?? ((message) => console.log(message));
3810
4259
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3811
4260
  const key = stripeKeys(appRoot).STRIPE_SECRET_KEY;
3812
4261
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
3813
4262
  const db = readD1Databases(appRoot)[0];
3814
4263
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
3815
- const appRequire = createRequire2(join20(appRoot, "package.json"));
4264
+ const appRequire = createRequire2(join22(appRoot, "package.json"));
3816
4265
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
3817
4266
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
3818
- const appName = JSON.parse(readFileSync13(join20(appRoot, "package.json"), "utf8")).name ?? "app";
4267
+ const appName = JSON.parse(readFileSync13(join22(appRoot, "package.json"), "utf8")).name ?? "app";
3819
4268
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
3820
4269
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
3821
4270
  if (products.length === 0) {
@@ -3836,88 +4285,88 @@ async function syncPlans(options2 = {}) {
3836
4285
  ${existingResult.output.trim()}`);
3837
4286
  }
3838
4287
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
3839
- const dir = mkdtempSync2(join20(tmpdir2(), "flare-sync-"));
4288
+ const dir = mkdtempSync3(join22(tmpdir3(), "flare-sync-"));
3840
4289
  try {
3841
- const file = join20(dir, "sync-plans.sql");
3842
- writeFileSync14(file, renderSyncSql(rows));
4290
+ const file = join22(dir, "sync-plans.sql");
4291
+ writeFileSync15(file, renderSyncSql(rows));
3843
4292
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3844
4293
  if (result.code !== 0) throw new Error(`Could not write plans:
3845
4294
  ${result.output.trim()}`);
3846
4295
  } finally {
3847
- rmSync3(dir, { recursive: true, force: true });
4296
+ rmSync4(dir, { recursive: true, force: true });
3848
4297
  }
3849
4298
  let created = 0;
3850
4299
  for (const row of rows) {
3851
4300
  const isNew = !known.has(row.stripePriceId);
3852
4301
  if (isNew) created++;
3853
4302
  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)}`);
4303
+ log2(`${(isNew ? pc18.green("create") : pc18.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc18.dim(price)}`);
3855
4304
  }
3856
- for (const name of skipped) log(pc16.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4305
+ for (const name of skipped) log2(pc18.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3857
4306
  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(`
4307
+ if (configuration) log2(`${pc18.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4308
+ log2(pc18.green(`
3860
4309
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
3861
4310
  return { created, updated: rows.length - created, skipped };
3862
4311
  }
3863
4312
 
3864
4313
  // src/commands/sync.ts
3865
4314
  import { existsSync as existsSync22 } from "fs";
3866
- import { join as join21 } from "path";
3867
- import pc17 from "picocolors";
4315
+ import { join as join23 } from "path";
4316
+ import pc19 from "picocolors";
3868
4317
  async function syncTypes(options2 = {}) {
3869
- const log = options2.log ?? ((message) => console.log(message));
4318
+ const log2 = options2.log ?? ((message) => console.log(message));
3870
4319
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3871
4320
  const all = await loadResources(appRoot);
3872
4321
  const policies = await loadPolicies(appRoot);
3873
- const tracksPolicies = policies.length > 0 || existsSync22(join21(appRoot, "policies/index.ts"));
4322
+ const tracksPolicies = policies.length > 0 || existsSync22(join23(appRoot, "policies/index.ts"));
3874
4323
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
3875
- if (!options2.check) ensureSupportFiles(appRoot, log);
4324
+ if (!options2.check) ensureSupportFiles(appRoot, log2);
3876
4325
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
3877
- logResults(results, log);
4326
+ logResults(results, log2);
3878
4327
  const drift = results.filter((r) => r.outcome === "skipped-drift");
3879
4328
  const unmarked = results.filter((r) => r.outcome === "skipped-unmarked");
3880
4329
  const changed = results.filter((r) => r.outcome === "create" || r.outcome === "update");
3881
4330
  const orphans = findOrphans(appRoot, all);
3882
4331
  if (drift.length) {
3883
- log(
4332
+ log2(
3884
4333
  `
3885
- ${pc17.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4334
+ ${pc19.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3886
4335
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
3887
4336
  );
3888
4337
  }
3889
4338
  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.`);
4339
+ log2(`
4340
+ ${pc19.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3892
4341
  }
3893
4342
  if (orphans.length) {
3894
4343
  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.`
4344
+ log2(`
4345
+ ${pc19.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4346
+ for (const orphan of orphans) log2(` ${orphan.path}`);
4347
+ log2(
4348
+ `If the resource was deleted, remove them with ${pc19.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
3900
4349
  );
3901
4350
  }
3902
4351
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
3903
4352
  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")}.`);
4353
+ log2(`
4354
+ ${pc19.cyan("Tables changed.")} Create a migration with ${pc19.bold("flare gen migration <name> --from-schema")}, then ${pc19.bold("flare migrate")}.`);
3906
4355
  }
3907
4356
  const problems = drift.length + unmarked.length + orphans.length;
3908
4357
  if (options2.check) {
3909
4358
  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")}.`);
4359
+ if (stale === 0 && problems === 0) log2(pc19.green("Generated files are in sync with the resource descriptors."));
4360
+ else log2(`
4361
+ ${pc19.red("Out of sync:")} ${stale} file(s) would change. Run ${pc19.bold("flare sync-types")}.`);
3913
4362
  return stale + problems > 0 ? 1 : 0;
3914
4363
  }
3915
- if (changed.length === 0 && problems === 0) log(pc17.green("Everything is in sync."));
4364
+ if (changed.length === 0 && problems === 0) log2(pc19.green("Everything is in sync."));
3916
4365
  return problems > 0 ? 1 : 0;
3917
4366
  }
3918
4367
 
3919
4368
  // src/commands/user-role.ts
3920
- import pc18 from "picocolors";
4369
+ import pc20 from "picocolors";
3921
4370
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
3922
4371
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
3923
4372
  function userRoleSql(email, role) {
@@ -3926,13 +4375,13 @@ function userRoleSql(email, role) {
3926
4375
  return `UPDATE "user" SET role = '${role}', updated_at = cast(unixepoch('subsecond') * 1000 as integer) WHERE lower(email) = lower('${email}') RETURNING email, role`;
3927
4376
  }
3928
4377
  async function setUserRole(email, role, options2 = {}) {
3929
- const log = options2.log ?? ((message) => console.log(message));
4378
+ const log2 = options2.log ?? ((message) => console.log(message));
3930
4379
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3931
4380
  const sql2 = userRoleSql(email, role);
3932
4381
  const roles = await listRoles(options2);
3933
4382
  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" : ""}`));
4383
+ log2(pc20.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4384
+ log2(pc20.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
3936
4385
  return 1;
3937
4386
  }
3938
4387
  const db = readD1Databases(appRoot)[0];
@@ -3946,10 +4395,10 @@ ${result.output.trim()}`);
3946
4395
  const [first] = extractJson(result.output);
3947
4396
  const updated = first?.results ?? [];
3948
4397
  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.`));
4398
+ log2(pc20.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
3950
4399
  return 1;
3951
4400
  }
3952
- log(pc18.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4401
+ log2(pc20.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3953
4402
  return 0;
3954
4403
  }
3955
4404
 
@@ -3959,8 +4408,27 @@ function createCli() {
3959
4408
  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
4409
  const install = !options2.skipInstall;
3961
4410
  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 });
4411
+ if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
4412
+ const result = createApp(dir, {
4413
+ pm: options2.pm,
4414
+ install,
4415
+ ...answers,
4416
+ log: (message) => prompts3.log.message(message),
4417
+ // The install is the long part of `flare create`, so it gets a timer rather
4418
+ // than a wall of package manager output. Output is kept for when it fails.
4419
+ installer: (packageManager, args, cwd) => {
4420
+ const spin = prompts3.spinner({ indicator: "timer" });
4421
+ spin.start(`Installing dependencies with ${packageManager}`);
4422
+ const { code, output } = runQuiet(packageManager, args, cwd);
4423
+ if (code === 0) {
4424
+ spin.stop(`Installed dependencies with ${packageManager}`);
4425
+ } else {
4426
+ spin.error(`${packageManager} install failed`);
4427
+ process.stderr.write(output);
4428
+ }
4429
+ return code;
4430
+ }
4431
+ });
3964
4432
  printNextSteps(result, install);
3965
4433
  });
3966
4434
  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(
@@ -3996,6 +4464,9 @@ function createCli() {
3996
4464
  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
4465
  await runSeeds({ names });
3998
4466
  });
4467
+ 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) => {
4468
+ await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
4469
+ });
3999
4470
  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
4471
  await makeSeed(name, options2);
4001
4472
  });
@@ -4022,7 +4493,7 @@ function createCli() {
4022
4493
  cli.command(`${name} [...args]`, spec.description).allowUnknownOptions();
4023
4494
  }
4024
4495
  cli.help();
4025
- cli.version(FLARE_VERSION2);
4496
+ cli.version(FLARE_VERSION3);
4026
4497
  return cli;
4027
4498
  }
4028
4499
 
@@ -4041,6 +4512,6 @@ try {
4041
4512
  }
4042
4513
  }
4043
4514
  } catch (error) {
4044
- console.error(pc19.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4515
+ console.error(pc21.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4045
4516
  process.exitCode = 1;
4046
4517
  }