@flaredev/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.ts
2
- import pc24 from "picocolors";
2
+ import pc25 from "picocolors";
3
3
 
4
4
  // src/cli.ts
5
5
  import { cac } from "cac";
6
- import pc23 from "picocolors";
6
+ import pc24 from "picocolors";
7
7
  import * as prompts3 from "@clack/prompts";
8
8
  import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
9
9
 
@@ -201,6 +201,9 @@ var NEXT_DEV_DEPENDENCIES = {
201
201
  prisma: "7.10.0",
202
202
  // Runs seeds: the generated Prisma client is TypeScript a bundler compiles.
203
203
  tsx: "^4.23.13",
204
+ // `flare deploy` and `npm run deploy`. Local rather than global, so the deploy works
205
+ // on a fresh clone and every app pins its own version.
206
+ vercel: "^60.1.3",
204
207
  "@types/node": "^22.20.3",
205
208
  "@types/react": "^19.3.0",
206
209
  "@types/react-dom": "^19.3.0",
@@ -216,7 +219,9 @@ var CLOUDFLARE_ONLY = [
216
219
  "migrations",
217
220
  "drizzle.config.ts",
218
221
  ".dev.vars.example",
219
- "worker-configuration.d.ts"
222
+ "worker-configuration.d.ts",
223
+ // The engine's Drizzle adapter; the Next.js overlay brings prisma-rows.ts instead.
224
+ "lib/resource/drizzle-rows.ts"
220
225
  ];
221
226
 
222
227
  // src/stack.ts
@@ -224,6 +229,10 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
224
229
  import { join } from "path";
225
230
  var STACKS = ["cloudflare", "next"];
226
231
  var isStack = (value) => STACKS.includes(value);
232
+ var STACK_LABELS = {
233
+ cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
234
+ next: "Next.js on Vercel, Neon Postgres and Prisma \u2014 real Postgres, Node runtime"
235
+ };
227
236
  function readStack(appRoot) {
228
237
  const file = join(appRoot, "package.json");
229
238
  if (!existsSync(file)) return "cloudflare";
@@ -281,6 +290,20 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? "
281
290
  const name = userAgent.split("/")[0];
282
291
  return isPackageManager(name) ? name : "pnpm";
283
292
  }
293
+ function isInstalled(packageManager) {
294
+ const probe = [packageManager, "--version"].join(" ");
295
+ const result = process.platform === "win32" ? spawnSync(probe, { stdio: "ignore", shell: true, env: childEnv() }) : spawnSync(packageManager, ["--version"], { stdio: "ignore", env: childEnv() });
296
+ return result.status === 0;
297
+ }
298
+ function choosePackageManager(explicit) {
299
+ if (explicit !== void 0) {
300
+ if (!isPackageManager(explicit)) throw new Error(`Unknown package manager "${explicit}". Use pnpm, npm, yarn, or bun.`);
301
+ return { packageManager: explicit, preferred: false };
302
+ }
303
+ const detected = detectPackageManager();
304
+ if (detected !== "pnpm" && isInstalled("pnpm")) return { packageManager: "pnpm", preferred: true };
305
+ return { packageManager: detected, preferred: false };
306
+ }
284
307
  function run(command, args, cwd) {
285
308
  const env = childEnv();
286
309
  const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true, env }) : spawnSync(command, args, { cwd, stdio: "inherit", env });
@@ -321,10 +344,7 @@ function createApp(target, options2 = {}) {
321
344
  if (existsSync3(dir) && readdirSync2(dir).length > 0) {
322
345
  throw new Error(`Directory ${dir} already exists and is not empty.`);
323
346
  }
324
- if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
325
- throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
326
- }
327
- const packageManager = options2.pm ?? detectPackageManager();
347
+ const { packageManager, preferred: prefersPnpm } = choosePackageManager(options2.pm);
328
348
  if (options2.stack !== void 0 && !isStack(options2.stack)) {
329
349
  throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
330
350
  }
@@ -398,6 +418,7 @@ ${devVarsEntries(authProviders)}`
398
418
  if (pinned) copyLockfile(packageManager, dir);
399
419
  const log2 = options2.log ?? ((message) => console.log(message));
400
420
  log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
421
+ if (prefersPnpm) log2(pc.dim(`Installing with pnpm \u2014 far quicker for an app this size. Use ${pc.bold("--pm npm")} to change that.`));
401
422
  return { dir, name, packageManager, inWorkspace };
402
423
  }
403
424
  function copyLockfile(packageManager, dir) {
@@ -1794,12 +1815,14 @@ function resolveBin(appRoot, pkgName, binName2) {
1794
1815
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1795
1816
  return join8(dirname4(manifestPath), relativeBin);
1796
1817
  }
1818
+ var NEXT_COMMANDS = {
1819
+ dev: { pkg: "next", bin: "next", args: ["dev"] },
1820
+ build: { pkg: "next", bin: "next", args: ["build"] },
1821
+ start: { pkg: "next", bin: "next", args: ["start"] },
1822
+ deploy: { pkg: "vercel", bin: "vercel", args: ["deploy", "--prod"] }
1823
+ };
1797
1824
  function delegatedArgv(appRoot, command, forwarded) {
1798
- if (readStack(appRoot) === "next") {
1799
- const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
1800
- throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
1801
- }
1802
- const spec = DELEGATED_COMMANDS[command];
1825
+ const spec = readStack(appRoot) === "next" ? NEXT_COMMANDS[command] : DELEGATED_COMMANDS[command];
1803
1826
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1804
1827
  }
1805
1828
  function runNode(argv, cwd) {
@@ -1854,15 +1877,22 @@ function portArg(args) {
1854
1877
  }
1855
1878
  async function runDelegated(command, forwarded, cwd = process.cwd()) {
1856
1879
  const appRoot = findAppRoot(cwd);
1880
+ const next = readStack(appRoot) === "next";
1857
1881
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1858
1882
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1859
1883
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1860
- if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
1884
+ if (next && command === "build" && !wantsHelp) {
1885
+ const code = await runNode([resolveBin(appRoot, "prisma", "prisma"), "generate"], appRoot);
1886
+ if (code !== 0) return code;
1887
+ }
1888
+ const built = next ? ".next" : "dist/server/wrangler.json";
1889
+ if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, built))) {
1861
1890
  console.log("No production build found; running `flare build` first.\n");
1862
- const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1891
+ const code = await runDelegated("build", [], appRoot);
1863
1892
  if (code !== 0) return code;
1864
1893
  }
1865
1894
  if (command === "deploy") {
1895
+ if (next) return runNode(delegatedArgv(appRoot, "deploy", forwarded), appRoot);
1866
1896
  return runDeploy(forwarded, {
1867
1897
  appRoot,
1868
1898
  wranglerBin: resolveBin(appRoot, "wrangler", "wrangler"),
@@ -1956,7 +1986,7 @@ function previousSnapshot(dir, migrationFile) {
1956
1986
 
1957
1987
  // src/generator/plan.ts
1958
1988
  import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
1959
- import { dirname as dirname5, join as join11 } from "path";
1989
+ import { dirname as dirname5, join as join11, sep } from "path";
1960
1990
  import pc5 from "picocolors";
1961
1991
 
1962
1992
  // src/generator/policy.ts
@@ -2281,14 +2311,19 @@ ${lines.join("\n")}
2281
2311
  var handlerImports = (entry, stack = "cloudflare") => {
2282
2312
  const local = resourceLocal(entry.stem);
2283
2313
  const source = stack === "next" ? {
2284
- imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
2314
+ imports: [`import { createResourceHandlers, prismaRows } from "@/lib/resource";`, `import { prisma } from "@/lib/db";`],
2285
2315
  wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2286
2316
  } : {
2287
- imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2288
- wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
2317
+ imports: [
2318
+ `import { createResourceHandlers, drizzleRows } from "@/lib/resource";`,
2319
+ `import { getDb } from "@/db";`,
2320
+ `import { ${tableExport(entry.resource)} } from "@/db/schema";`
2321
+ ],
2322
+ // Named rather than implied: the adapter is the one thing about this route
2323
+ // that differs between stacks, so it is written out.
2324
+ wiring: [` rows: drizzleRows(${tableExport(entry.resource)}, getDb),`]
2289
2325
  };
2290
2326
  return [
2291
- `import { createResourceHandlers } from "@flaredev/core/server";`,
2292
2327
  ...source.imports,
2293
2328
  `import { authorize, currentUser } from "@/lib/api";`,
2294
2329
  `import { revalidateResource } from "@/lib/cache";`,
@@ -2322,6 +2357,49 @@ function renderItemRoute(entry, stack = "cloudflare") {
2322
2357
  var adminImports = (entry) => [
2323
2358
  `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`
2324
2359
  ];
2360
+ function renderAdminListLoading(entry) {
2361
+ const columns = Math.min(6, storedFields2(entry.resource).length + 1);
2362
+ return [
2363
+ `import { ResourceListSkeleton } from "@/components/dashboard/skeletons";`,
2364
+ "",
2365
+ `export default function Loading() {`,
2366
+ ` return <ResourceListSkeleton columns={${columns}} />;`,
2367
+ "}",
2368
+ ""
2369
+ ].join("\n");
2370
+ }
2371
+ function renderAdminFormLoading(entry) {
2372
+ const fields = Math.min(8, Math.max(1, storedFields2(entry.resource).length));
2373
+ return [
2374
+ `import { FormSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
2375
+ "",
2376
+ `export default function Loading() {`,
2377
+ " return (",
2378
+ " <>",
2379
+ " <PageHeaderSkeleton actions={0} />",
2380
+ ` <FormSkeleton fields={${fields}} />`,
2381
+ " </>",
2382
+ " );",
2383
+ "}",
2384
+ ""
2385
+ ].join("\n");
2386
+ }
2387
+ function renderAdminDetailLoading(entry) {
2388
+ const rows = Math.min(10, Math.max(1, storedFields2(entry.resource).length));
2389
+ return [
2390
+ `import { DetailSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
2391
+ "",
2392
+ `export default function Loading() {`,
2393
+ " return (",
2394
+ " <>",
2395
+ " <PageHeaderSkeleton />",
2396
+ ` <DetailSkeleton rows={${rows}} />`,
2397
+ " </>",
2398
+ " );",
2399
+ "}",
2400
+ ""
2401
+ ].join("\n");
2402
+ }
2325
2403
  function renderAdminListPage(entry) {
2326
2404
  const local = resourceLocal(entry.stem);
2327
2405
  return [
@@ -2464,7 +2542,11 @@ function resourceFiles(entry, all, stack = "cloudflare") {
2464
2542
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2465
2543
  { path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
2466
2544
  { path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2467
- { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
2545
+ { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) },
2546
+ { path: `app/dashboard/${resource.slug}/loading.tsx`, content: renderAdminListLoading(entry) },
2547
+ { path: `app/dashboard/${resource.slug}/new/loading.tsx`, content: renderAdminFormLoading(entry) },
2548
+ { path: `app/dashboard/${resource.slug}/[id]/loading.tsx`, content: renderAdminDetailLoading(entry) },
2549
+ { path: `app/dashboard/${resource.slug}/[id]/edit/loading.tsx`, content: renderAdminFormLoading(entry) }
2468
2550
  ];
2469
2551
  }
2470
2552
 
@@ -2546,7 +2628,9 @@ function findOrphans(appRoot, all) {
2546
2628
  for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2547
2629
  }
2548
2630
  for (const dir of list2("app/dashboard", () => true)) {
2549
- for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2631
+ const pages = [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`];
2632
+ const skeletons = [`${dir}/loading.tsx`, `${dir}/new/loading.tsx`, `${dir}/[id]/loading.tsx`, `${dir}/[id]/edit/loading.tsx`];
2633
+ for (const file of [...pages, ...skeletons]) {
2550
2634
  if (existsSync12(join11(appRoot, file))) candidates.push(file);
2551
2635
  }
2552
2636
  }
@@ -2558,11 +2642,19 @@ function findOrphans(appRoot, all) {
2558
2642
  return orphans;
2559
2643
  }
2560
2644
  function ensureSupportFiles(appRoot, log2) {
2561
- for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2645
+ const stack = readStack(appRoot);
2646
+ const engine = ["rows.ts", "query.ts", "store.ts", "handlers.ts", "index.ts", stack === "next" ? "prisma-rows.ts" : "drizzle-rows.ts"].map(
2647
+ (name) => `lib/resource/${name}`
2648
+ );
2649
+ for (const path of ["lib/api.ts", "lib/cache.ts", ...engine]) {
2562
2650
  const target = join11(appRoot, path);
2563
2651
  if (existsSync12(target)) continue;
2652
+ const source = [join11(templatesDir, "next", path), join11(templatesDir, "app", path)].find(
2653
+ (candidate) => (stack === "next" || !candidate.includes(`${sep}next${sep}`)) && existsSync12(candidate)
2654
+ );
2655
+ if (!source) continue;
2564
2656
  mkdirSync4(dirname5(target), { recursive: true });
2565
- copyFileSync2(join11(templatesDir, "app", path), target);
2657
+ copyFileSync2(source, target);
2566
2658
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2567
2659
  }
2568
2660
  }
@@ -2606,30 +2698,30 @@ function addStripeDependency(appRoot) {
2606
2698
  return true;
2607
2699
  }
2608
2700
  function writeBillingResource(appRoot, name, force, log2) {
2609
- const relative2 = descriptorPath(name);
2610
- const path = join12(appRoot, relative2);
2701
+ const relative3 = descriptorPath(name);
2702
+ const path = join12(appRoot, relative3);
2611
2703
  if (!existsSync13(path)) {
2612
2704
  mkdirSync5(dirname6(path), { recursive: true });
2613
2705
  writeFileSync7(path, renderBillingDescriptor(name));
2614
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2706
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2615
2707
  return;
2616
2708
  }
2617
2709
  const parts = splitMarkers(readFileSync9(path, "utf8"));
2618
- 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]}`);
2710
+ if (!parts) throw new Error(`${relative3} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
2619
2711
  const merged = mergeBillingFields(name, parts.block);
2620
2712
  if (!merged) {
2621
- log2(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2713
+ log2(`${pc6.dim("identical".padEnd(9))} ${relative3} (has every billing field)`);
2622
2714
  return;
2623
2715
  }
2624
2716
  try {
2625
2717
  writeGenerated(path, merged, { force });
2626
2718
  } catch (error) {
2627
2719
  if (error instanceof DriftError) {
2628
- throw new Error(`${relative2}: its fields block was edited by hand, so the billing fields weren't merged. Pass --force, or add them yourself: ${BILLING_RESOURCES[name]}`);
2720
+ throw new Error(`${relative3}: its fields block was edited by hand, so the billing fields weren't merged. Pass --force, or add them yourself: ${BILLING_RESOURCES[name]}`);
2629
2721
  }
2630
2722
  throw error;
2631
2723
  }
2632
- log2(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2724
+ log2(`${pc6.yellow("update".padEnd(9))} ${relative3} (added billing fields, kept yours)`);
2633
2725
  }
2634
2726
  async function genBilling(options2 = {}) {
2635
2727
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -2637,14 +2729,14 @@ async function genBilling(options2 = {}) {
2637
2729
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2638
2730
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2639
2731
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2640
- const relative2 = policyPath(name);
2641
- if (existsSync13(join12(appRoot, relative2))) {
2642
- log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2732
+ const relative3 = policyPath(name);
2733
+ if (existsSync13(join12(appRoot, relative3))) {
2734
+ log2(`${pc6.dim("kept".padEnd(9))} ${relative3} (already exists)`);
2643
2735
  continue;
2644
2736
  }
2645
2737
  mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2646
- writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2647
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2738
+ writeFileSync7(join12(appRoot, relative3), renderPolicy(name, roles));
2739
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2648
2740
  }
2649
2741
  const files = [];
2650
2742
  const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
@@ -3229,15 +3321,15 @@ function securityFiles(appName) {
3229
3321
  ];
3230
3322
  }
3231
3323
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
3232
- function writeOnce(appRoot, relative2, content, log2) {
3233
- const path = join13(appRoot, relative2);
3324
+ function writeOnce(appRoot, relative3, content, log2) {
3325
+ const path = join13(appRoot, relative3);
3234
3326
  if (existsSync14(path)) {
3235
- log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
3327
+ log2(`${pc7.dim("kept".padEnd(9))} ${relative3} (already exists)`);
3236
3328
  return;
3237
3329
  }
3238
3330
  mkdirSync6(dirname7(path), { recursive: true });
3239
3331
  writeFileSync8(path, content);
3240
- log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
3332
+ log2(`${pc7.green("create".padEnd(9))} ${relative3}`);
3241
3333
  }
3242
3334
  async function genSecurity(options2 = {}) {
3243
3335
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -3351,6 +3443,15 @@ function stopIfCancelled(value) {
3351
3443
  async function askCreateQuestions(given, appName) {
3352
3444
  prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
3353
3445
  const answers = { ...given };
3446
+ if (given.stack === void 0) {
3447
+ answers.stack = stopIfCancelled(
3448
+ await prompts2.select({
3449
+ message: "Where will it run?",
3450
+ initialValue: "cloudflare",
3451
+ options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
3452
+ })
3453
+ );
3454
+ }
3354
3455
  if (given.theme === void 0) {
3355
3456
  answers.theme = stopIfCancelled(
3356
3457
  await prompts2.select({
@@ -3500,16 +3601,16 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3500
3601
  const action = options2.action ?? (method === "GET" ? "read" : "update");
3501
3602
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3502
3603
  const { resource, stem } = entry;
3503
- const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3504
- const path = join16(appRoot, relative2);
3505
- if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
3604
+ const relative3 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3605
+ const path = join16(appRoot, relative3);
3606
+ if (existsSync17(path)) throw new Error(`${relative3} already exists.`);
3506
3607
  mkdirSync8(join16(path, ".."), { recursive: true });
3507
3608
  const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3508
3609
  writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3509
- log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3610
+ log2(`${pc11.green("create".padEnd(9))} ${relative3}`);
3510
3611
  log2(`
3511
3612
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
3512
- return relative2;
3613
+ return relative3;
3513
3614
  }
3514
3615
  function renderEndpoint({ resource, stem, method, action, record, route }) {
3515
3616
  const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
@@ -3599,8 +3700,8 @@ async function genPolicy(rawName, options2) {
3599
3700
  const known = resources.map(({ resource }) => resource.name).join(", ") || "none";
3600
3701
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3601
3702
  }
3602
- const relative2 = policyPath(name);
3603
- const path = join18(appRoot, relative2);
3703
+ const relative3 = policyPath(name);
3704
+ const path = join18(appRoot, relative3);
3604
3705
  const exists = existsSync19(path);
3605
3706
  let roles;
3606
3707
  let deleteRoles;
@@ -3614,14 +3715,14 @@ async function genPolicy(rawName, options2) {
3614
3715
  if (!exists) {
3615
3716
  mkdirSync10(dirname9(path), { recursive: true });
3616
3717
  writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3617
- log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3718
+ log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
3618
3719
  } else {
3619
3720
  try {
3620
3721
  const outcome = writeGenerated(path, block, { force: options2.force });
3621
- log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative2}`);
3722
+ log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative3}`);
3622
3723
  } catch (error) {
3623
3724
  if (error instanceof DriftError) {
3624
- throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
3725
+ throw new Error(`The roles in ${relative3} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
3625
3726
  }
3626
3727
  throw error;
3627
3728
  }
@@ -3631,7 +3732,7 @@ async function genPolicy(rawName, options2) {
3631
3732
  logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3632
3733
  log2(`
3633
3734
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3634
- return { name, path: relative2 };
3735
+ return { name, path: relative3 };
3635
3736
  }
3636
3737
 
3637
3738
  // src/commands/migrate.ts
@@ -3980,12 +4081,118 @@ ${result.output.trim()}`);
3980
4081
  return 0;
3981
4082
  }
3982
4083
 
4084
+ // src/commands/eject.ts
4085
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync15 } from "fs";
4086
+ import { dirname as dirname11, join as join21, relative as relative2 } from "path";
4087
+ import pc17 from "picocolors";
4088
+ var TRACKED = ["lib/resource"];
4089
+ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only) => path === only || path.startsWith(`${only}/`));
4090
+ function upstreamFile(path, stack) {
4091
+ if (prunedOn(path, stack)) return void 0;
4092
+ const candidates = stack === "next" ? [join21(templatesDir, "next", path), join21(templatesDir, "app", path)] : [join21(templatesDir, "app", path)];
4093
+ return candidates.find((candidate) => existsSync22(candidate));
4094
+ }
4095
+ function trackedPaths(appRoot, stack) {
4096
+ const paths = /* @__PURE__ */ new Set();
4097
+ const walk = (base, dir) => {
4098
+ if (!existsSync22(join21(base, dir))) return;
4099
+ for (const entry of readdirSync8(join21(base, dir), { withFileTypes: true })) {
4100
+ const next = `${dir}/${entry.name}`;
4101
+ if (entry.isDirectory()) walk(base, next);
4102
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
4103
+ }
4104
+ };
4105
+ for (const root of TRACKED) {
4106
+ walk(appRoot, root);
4107
+ walk(join21(templatesDir, "app"), root);
4108
+ if (stack === "next") walk(join21(templatesDir, "next"), root);
4109
+ }
4110
+ return [...paths].filter((path) => upstreamFile(path, stack) !== void 0).sort();
4111
+ }
4112
+ function compareTracked(appRoot, stack, filter) {
4113
+ return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
4114
+ const source = upstreamFile(path, stack);
4115
+ const theirs = readFileSync14(source, "utf8");
4116
+ const target = join21(appRoot, path);
4117
+ if (!existsSync22(target)) return { path, state: "missing", theirs };
4118
+ const mine = readFileSync14(target, "utf8");
4119
+ return { path, state: mine === theirs ? "same" : "changed", mine, theirs };
4120
+ });
4121
+ }
4122
+ function summarise(mine, theirs) {
4123
+ const a = mine.split("\n");
4124
+ const b = theirs.split("\n");
4125
+ const lines = [];
4126
+ const max = Math.max(a.length, b.length);
4127
+ let shown = 0;
4128
+ for (let index = 0; index < max && shown < 12; index += 1) {
4129
+ if (a[index] === b[index]) continue;
4130
+ if (a[index] !== void 0) lines.push(pc17.red(` - ${a[index].slice(0, 100)}`));
4131
+ if (b[index] !== void 0) lines.push(pc17.green(` + ${b[index].slice(0, 100)}`));
4132
+ shown += 1;
4133
+ }
4134
+ return lines;
4135
+ }
4136
+ function diffTracked(options2 = {}) {
4137
+ const log2 = options2.log ?? ((message) => console.log(message));
4138
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4139
+ const stack = readStack(appRoot);
4140
+ const comparisons = compareTracked(appRoot, stack, options2.filter);
4141
+ const changed = comparisons.filter((entry) => entry.state !== "same");
4142
+ if (changed.length === 0) {
4143
+ log2(pc17.green(`Up to date: ${comparisons.length} file(s) match this version of Flare.`));
4144
+ return comparisons;
4145
+ }
4146
+ log2(pc17.bold(`${changed.length} of ${comparisons.length} file(s) differ from this version of Flare:
4147
+ `));
4148
+ for (const entry of changed) {
4149
+ if (entry.state === "missing") {
4150
+ log2(`${pc17.yellow("missing")} ${entry.path} ${pc17.dim("(Flare ships this; your app doesn't have it)")}`);
4151
+ continue;
4152
+ }
4153
+ log2(`${pc17.cyan("changed")} ${entry.path}`);
4154
+ for (const line of summarise(entry.mine, entry.theirs)) log2(line);
4155
+ log2("");
4156
+ }
4157
+ log2(pc17.dim(`Yours is "-", Flare's is "+". Apply with ${pc17.bold("flare update")}, or keep yours and do nothing.`));
4158
+ return comparisons;
4159
+ }
4160
+ function updateTracked(options2 = {}) {
4161
+ const log2 = options2.log ?? ((message) => console.log(message));
4162
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4163
+ const stack = readStack(appRoot);
4164
+ const comparisons = compareTracked(appRoot, stack, options2.filter);
4165
+ const changed = comparisons.filter((entry) => entry.state !== "same");
4166
+ if (changed.length === 0) {
4167
+ log2(pc17.green("Nothing to update."));
4168
+ return 0;
4169
+ }
4170
+ if (!options2.yes) {
4171
+ log2(pc17.bold(`${changed.length} file(s) would be overwritten with Flare's version:
4172
+ `));
4173
+ for (const entry of changed) log2(` ${entry.path}`);
4174
+ log2(pc17.yellow(`
4175
+ This discards your edits to those files. Re-run with ${pc17.bold("--yes")} to proceed.`));
4176
+ log2(pc17.dim(`See what would change first with ${pc17.bold("flare diff")}.`));
4177
+ return 1;
4178
+ }
4179
+ for (const entry of changed) {
4180
+ const target = join21(appRoot, entry.path);
4181
+ mkdirSync11(dirname11(target), { recursive: true });
4182
+ writeFileSync15(target, entry.theirs);
4183
+ log2(`${pc17.green("update")} ${relative2(process.cwd(), target) || entry.path}`);
4184
+ }
4185
+ log2(pc17.green(`
4186
+ Updated ${changed.length} file(s).`));
4187
+ return 0;
4188
+ }
4189
+
3983
4190
  // src/commands/seed.ts
3984
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
3985
- import { join as join22 } from "path";
4191
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
4192
+ import { join as join23 } from "path";
3986
4193
  import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
3987
4194
  import { createJiti as createJiti4 } from "jiti";
3988
- import pc17 from "picocolors";
4195
+ import pc18 from "picocolors";
3989
4196
 
3990
4197
  // src/seed/bulk.ts
3991
4198
  var MAX_STATEMENT_BYTES = 8e4;
@@ -4079,17 +4286,20 @@ function createInsertMany(sink, drizzle, onProgress) {
4079
4286
  }
4080
4287
 
4081
4288
  // src/seed/local-d1.ts
4289
+ import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4082
4290
  import { createRequire } from "module";
4083
- import { join as join21 } from "path";
4291
+ import { join as join22 } from "path";
4084
4292
  import { pathToFileURL } from "url";
4293
+ import { parse as parseJsonc3 } from "jsonc-parser";
4085
4294
  var LOCAL_STATE = ".wrangler/state/v3";
4086
4295
  async function openLocalD1(appRoot, binding) {
4087
4296
  process.env.WRANGLER_LOG ??= "error";
4088
- const appRequire = createRequire(join21(appRoot, "package.json"));
4297
+ const appRequire = createRequire(join22(appRoot, "package.json"));
4089
4298
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
4299
+ const configPath = seedConfig(appRoot);
4090
4300
  const { env, dispose } = await wrangler.getPlatformProxy({
4091
- configPath: join21(appRoot, "wrangler.jsonc"),
4092
- persist: { path: join21(appRoot, LOCAL_STATE) }
4301
+ configPath,
4302
+ persist: { path: join22(appRoot, LOCAL_STATE) }
4093
4303
  });
4094
4304
  const bindings = env;
4095
4305
  const name = binding ?? "DB";
@@ -4098,14 +4308,30 @@ async function openLocalD1(appRoot, binding) {
4098
4308
  const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
4099
4309
  throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
4100
4310
  }
4101
- return { db, env: bindings, dispose };
4311
+ return {
4312
+ db,
4313
+ env: bindings,
4314
+ dispose: async () => {
4315
+ await dispose();
4316
+ rmSync4(configPath, { force: true });
4317
+ }
4318
+ };
4319
+ }
4320
+ function seedConfig(appRoot) {
4321
+ const source = join22(appRoot, "wrangler.jsonc");
4322
+ const config = parseJsonc3(readFileSync15(source, "utf8")) ?? {};
4323
+ delete config.durable_objects;
4324
+ delete config.migrations;
4325
+ const path = join22(appRoot, ".flare-seed.wrangler.json");
4326
+ writeFileSync16(path, JSON.stringify(config, null, 2));
4327
+ return path;
4102
4328
  }
4103
4329
 
4104
4330
  // src/commands/seed.ts
4105
4331
  var SEEDS_DIR = "seeds";
4106
4332
  function seedFiles(appRoot, names) {
4107
- const dir = join22(appRoot, SEEDS_DIR);
4108
- const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4333
+ const dir = join23(appRoot, SEEDS_DIR);
4334
+ const all = existsSync23(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4109
4335
  if (!names?.length) return all;
4110
4336
  return names.map((name) => {
4111
4337
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -4120,28 +4346,28 @@ async function runSeeds(options2 = {}) {
4120
4346
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4121
4347
  const files = seedFiles(appRoot, options2.names);
4122
4348
  if (files.length === 0) {
4123
- log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
4349
+ log2(`No seeds found. Create one with ${pc18.bold("flare seed:make <name>")}.`);
4124
4350
  return;
4125
4351
  }
4126
4352
  if (readStack(appRoot) === "next") {
4127
4353
  await runPrismaSeeds(appRoot, files, log2);
4128
4354
  return;
4129
4355
  }
4130
- const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4356
+ const jiti = createJiti4(join23(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4131
4357
  const { drizzle } = await jiti.import("drizzle-orm/d1");
4132
4358
  const drizzleHelpers = await jiti.import("drizzle-orm");
4133
- const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
4359
+ const schema = await jiti.import(join23(appRoot, "db/schema.ts"));
4134
4360
  const local = await openLocalD1(appRoot);
4135
4361
  try {
4136
4362
  const db = drizzle(local.db, { schema });
4137
4363
  const insertMany = createInsertMany(local.db, drizzleHelpers);
4138
4364
  for (const file of files) {
4139
- const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
4365
+ const mod = await jiti.import(join23(appRoot, SEEDS_DIR, file));
4140
4366
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
4141
- log2(`${pc17.cyan("seed")} ${file}`);
4367
+ log2(`${pc18.cyan("seed")} ${file}`);
4142
4368
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
4143
4369
  }
4144
- log2(pc17.green(`
4370
+ log2(pc18.green(`
4145
4371
  Ran ${files.length} seed(s) against the local database.`));
4146
4372
  } finally {
4147
4373
  await local.dispose();
@@ -4252,9 +4478,9 @@ async function makeSeed(rawName, options2 = {}) {
4252
4478
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4253
4479
  const name = kebabCase4(rawName);
4254
4480
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
4255
- const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
4256
- const path = join22(appRoot, relative2);
4257
- if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
4481
+ const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
4482
+ const path = join23(appRoot, relative3);
4483
+ if (existsSync23(path)) throw new Error(`${relative3} already exists.`);
4258
4484
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4259
4485
  let resource;
4260
4486
  if (options2.resource) {
@@ -4263,15 +4489,15 @@ async function makeSeed(rawName, options2 = {}) {
4263
4489
  } else {
4264
4490
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
4265
4491
  }
4266
- mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4267
- writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
4268
- log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
4492
+ mkdirSync12(join23(appRoot, SEEDS_DIR), { recursive: true });
4493
+ writeFileSync17(path, renderSeed(resource, readStack(appRoot)));
4494
+ log2(`${pc18.green("create".padEnd(9))} ${relative3}`);
4269
4495
  log2(`
4270
- Run it with ${pc17.bold(`flare seed ${name}`)}.`);
4271
- return relative2;
4496
+ Run it with ${pc18.bold(`flare seed ${name}`)}.`);
4497
+ return relative3;
4272
4498
  }
4273
4499
  async function runPrismaSeeds(appRoot, files, log2) {
4274
- const runner = join22(appRoot, ".flare-seed-runner.mts");
4500
+ const runner = join23(appRoot, ".flare-seed-runner.mts");
4275
4501
  const script = [
4276
4502
  `import "dotenv/config";`,
4277
4503
  `import { createFake } from "@flaredev/core";`,
@@ -4299,23 +4525,23 @@ async function runPrismaSeeds(appRoot, files, log2) {
4299
4525
  `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4300
4526
  ""
4301
4527
  ].join("\n");
4302
- writeFileSync15(runner, script);
4528
+ writeFileSync17(runner, script);
4303
4529
  try {
4304
4530
  const tsx = resolveBin(appRoot, "tsx", "tsx");
4305
4531
  const code = await runNode([tsx, runner], appRoot);
4306
4532
  if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4307
- log2(pc17.green(`
4533
+ log2(pc18.green(`
4308
4534
  Ran ${files.length} seed(s).`));
4309
4535
  } finally {
4310
- rmSync4(runner, { force: true });
4536
+ rmSync5(runner, { force: true });
4311
4537
  }
4312
4538
  }
4313
4539
 
4314
4540
  // src/commands/db-push.ts
4315
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
4541
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
4316
4542
  import { tmpdir as tmpdir2 } from "os";
4317
- import { join as join23 } from "path";
4318
- import pc18 from "picocolors";
4543
+ import { join as join24 } from "path";
4544
+ import pc19 from "picocolors";
4319
4545
 
4320
4546
  // src/terminal.ts
4321
4547
  var isTTY = () => Boolean(process.stdout.isTTY);
@@ -4379,7 +4605,7 @@ async function dbPush(tables, options2 = {}) {
4379
4605
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4380
4606
  const started = Date.now();
4381
4607
  const local = await openLocalD1(appRoot, options2.database);
4382
- const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4608
+ const dir = mkdtempSync2(join24(tmpdir2(), "flare-push-"));
4383
4609
  try {
4384
4610
  const statements = [];
4385
4611
  const counts = [];
@@ -4398,33 +4624,33 @@ async function dbPush(tables, options2 = {}) {
4398
4624
  offset += PAGE;
4399
4625
  }
4400
4626
  counts.push({ table, rows });
4401
- log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
4627
+ log2(`${pc19.cyan("read")} ${table} ${pc19.dim(`(${formatCount(rows)} rows)`)}`);
4402
4628
  }
4403
4629
  const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
4404
4630
  if (total === 0) {
4405
4631
  log2("Nothing to push: those tables are empty locally.");
4406
4632
  return { tables: counts, ms: Date.now() - started };
4407
4633
  }
4408
- const file = join23(dir, "push.sql");
4409
- writeFileSync16(file, statements.join("\n"));
4634
+ const file = join24(dir, "push.sql");
4635
+ writeFileSync18(file, statements.join("\n"));
4410
4636
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4411
4637
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4412
4638
  if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
4413
4639
  ${result.output.trim()}`);
4414
4640
  const ms = Date.now() - started;
4415
- log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4641
+ log2(`${pc19.green("\u2714")} Pushed ${pc19.bold(formatCount(total))} rows to ${pc19.bold(db.database_name)} ${pc19.dim(`(${formatDuration(ms)})`)}`);
4416
4642
  return { tables: counts, ms };
4417
4643
  } finally {
4418
- rmSync5(dir, { recursive: true, force: true });
4644
+ rmSync6(dir, { recursive: true, force: true });
4419
4645
  await local.dispose();
4420
4646
  }
4421
4647
  }
4422
4648
 
4423
4649
  // src/commands/seed-resource.ts
4424
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
4650
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
4425
4651
  import { tmpdir as tmpdir3 } from "os";
4426
- import { join as join24 } from "path";
4427
- import pc19 from "picocolors";
4652
+ import { join as join25 } from "path";
4653
+ import pc20 from "picocolors";
4428
4654
 
4429
4655
  // src/seed/rows.ts
4430
4656
  import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
@@ -4608,13 +4834,13 @@ async function seedResourceCommand(name, options2 = {}) {
4608
4834
  ...options2,
4609
4835
  onProgress: (written, total) => {
4610
4836
  const elapsed = Date.now() - started;
4611
- progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
4837
+ progress.update(` ${pc20.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc20.dim(formatRate(written, elapsed))}`);
4612
4838
  }
4613
4839
  });
4614
4840
  progress.done();
4615
4841
  const { rows, ms, where } = result;
4616
4842
  console.log(
4617
- `${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4843
+ `${pc20.green("\u2714")} Seeded ${pc20.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc20.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4618
4844
  );
4619
4845
  return result;
4620
4846
  } catch (error) {
@@ -4662,8 +4888,8 @@ ${result.output.trim()}`);
4662
4888
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4663
4889
  startIndex = counted?.total ?? 0;
4664
4890
  }
4665
- const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4666
- const file = join24(dir, `${resource.table}.sql`);
4891
+ const dir = mkdtempSync3(join25(tmpdir3(), "flare-seed-"));
4892
+ const file = join25(dir, `${resource.table}.sql`);
4667
4893
  try {
4668
4894
  const statements = [];
4669
4895
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4673,13 +4899,13 @@ ${result.output.trim()}`);
4673
4899
  written += statement.rows;
4674
4900
  options2.onProgress?.(written, total);
4675
4901
  }
4676
- writeFileSync17(file, statements.join("\n"));
4677
- log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4902
+ writeFileSync19(file, statements.join("\n"));
4903
+ log2(`${pc20.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc20.bold(db.database_name)}`);
4678
4904
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4679
4905
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4680
4906
  ${result.output.trim()}`);
4681
4907
  } finally {
4682
- rmSync6(dir, { recursive: true, force: true });
4908
+ rmSync7(dir, { recursive: true, force: true });
4683
4909
  }
4684
4910
  return { resource, rows: total, ms: Date.now() - started, where };
4685
4911
  }
@@ -4706,17 +4932,17 @@ ${result.output.trim()}`);
4706
4932
 
4707
4933
  // src/commands/billing-sync.ts
4708
4934
  import { createRequire as createRequire2 } from "module";
4709
- import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
4935
+ import { existsSync as existsSync24, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
4710
4936
  import { tmpdir as tmpdir4 } from "os";
4711
- import { join as join25 } from "path";
4937
+ import { join as join26 } from "path";
4712
4938
  import { pathToFileURL as pathToFileURL2 } from "url";
4713
4939
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4714
- import pc20 from "picocolors";
4940
+ import pc21 from "picocolors";
4715
4941
  function stripeKeys(appRoot) {
4716
4942
  const map = {};
4717
- const path = join25(appRoot, ".dev.vars");
4718
- if (existsSync23(path)) {
4719
- for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
4943
+ const path = join26(appRoot, ".dev.vars");
4944
+ if (existsSync24(path)) {
4945
+ for (const line of readFileSync16(path, "utf8").split(/\r?\n/)) {
4720
4946
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4721
4947
  if (match) map[match[1]] = match[2].trim();
4722
4948
  }
@@ -4799,10 +5025,10 @@ async function syncPlans(options2 = {}) {
4799
5025
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4800
5026
  const db = readD1Databases(appRoot)[0];
4801
5027
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4802
- const appRequire = createRequire2(join25(appRoot, "package.json"));
5028
+ const appRequire = createRequire2(join26(appRoot, "package.json"));
4803
5029
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4804
5030
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4805
- const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
5031
+ const appName = JSON.parse(readFileSync16(join26(appRoot, "package.json"), "utf8")).name ?? "app";
4806
5032
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4807
5033
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4808
5034
  if (products.length === 0) {
@@ -4823,41 +5049,41 @@ async function syncPlans(options2 = {}) {
4823
5049
  ${existingResult.output.trim()}`);
4824
5050
  }
4825
5051
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4826
- const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
5052
+ const dir = mkdtempSync4(join26(tmpdir4(), "flare-sync-"));
4827
5053
  try {
4828
- const file = join25(dir, "sync-plans.sql");
4829
- writeFileSync18(file, renderSyncSql(rows));
5054
+ const file = join26(dir, "sync-plans.sql");
5055
+ writeFileSync20(file, renderSyncSql(rows));
4830
5056
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4831
5057
  if (result.code !== 0) throw new Error(`Could not write plans:
4832
5058
  ${result.output.trim()}`);
4833
5059
  } finally {
4834
- rmSync7(dir, { recursive: true, force: true });
5060
+ rmSync8(dir, { recursive: true, force: true });
4835
5061
  }
4836
5062
  let created = 0;
4837
5063
  for (const row of rows) {
4838
5064
  const isNew = !known.has(row.stripePriceId);
4839
5065
  if (isNew) created++;
4840
5066
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
4841
- log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
5067
+ log2(`${(isNew ? pc21.green("create") : pc21.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc21.dim(price)}`);
4842
5068
  }
4843
- for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
5069
+ for (const name of skipped) log2(pc21.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4844
5070
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
4845
- if (configuration) log2(`${pc20.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
4846
- log2(pc20.green(`
5071
+ if (configuration) log2(`${pc21.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
5072
+ log2(pc21.green(`
4847
5073
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
4848
5074
  return { created, updated: rows.length - created, skipped };
4849
5075
  }
4850
5076
 
4851
5077
  // src/commands/sync.ts
4852
- import { existsSync as existsSync24 } from "fs";
4853
- import { join as join26 } from "path";
4854
- import pc21 from "picocolors";
5078
+ import { existsSync as existsSync25 } from "fs";
5079
+ import { join as join27 } from "path";
5080
+ import pc22 from "picocolors";
4855
5081
  async function syncTypes(options2 = {}) {
4856
5082
  const log2 = options2.log ?? ((message) => console.log(message));
4857
5083
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4858
5084
  const all = await loadResources(appRoot);
4859
5085
  const policies = await loadPolicies(appRoot);
4860
- const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
5086
+ const tracksPolicies = policies.length > 0 || existsSync25(join27(appRoot, "policies/index.ts"));
4861
5087
  const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4862
5088
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4863
5089
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -4869,42 +5095,42 @@ async function syncTypes(options2 = {}) {
4869
5095
  if (drift.length) {
4870
5096
  log2(
4871
5097
  `
4872
- ${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
5098
+ ${pc22.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4873
5099
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
4874
5100
  );
4875
5101
  }
4876
5102
  if (unmarked.length) {
4877
5103
  log2(`
4878
- ${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
5104
+ ${pc22.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4879
5105
  }
4880
5106
  if (orphans.length) {
4881
5107
  const resources = [...new Set(orphans.map((o) => o.resource))];
4882
5108
  log2(`
4883
- ${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
5109
+ ${pc22.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4884
5110
  for (const orphan of orphans) log2(` ${orphan.path}`);
4885
5111
  log2(
4886
- `If the resource was deleted, remove them with ${pc21.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
5112
+ `If the resource was deleted, remove them with ${pc22.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
4887
5113
  );
4888
5114
  }
4889
5115
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
4890
5116
  if (tablesChanged && !options2.check) {
4891
5117
  log2(`
4892
- ${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
5118
+ ${pc22.cyan("Tables changed.")} Create a migration with ${pc22.bold("flare gen migration <name> --from-schema")}, then ${pc22.bold("flare migrate")}.`);
4893
5119
  }
4894
5120
  const problems = drift.length + unmarked.length + orphans.length;
4895
5121
  if (options2.check) {
4896
5122
  const stale = changed.length;
4897
- if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
5123
+ if (stale === 0 && problems === 0) log2(pc22.green("Generated files are in sync with the resource descriptors."));
4898
5124
  else log2(`
4899
- ${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
5125
+ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flare sync-types")}.`);
4900
5126
  return stale + problems > 0 ? 1 : 0;
4901
5127
  }
4902
- if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
5128
+ if (changed.length === 0 && problems === 0) log2(pc22.green("Everything is in sync."));
4903
5129
  return problems > 0 ? 1 : 0;
4904
5130
  }
4905
5131
 
4906
5132
  // src/commands/user-role.ts
4907
- import pc22 from "picocolors";
5133
+ import pc23 from "picocolors";
4908
5134
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
4909
5135
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
4910
5136
  function userRoleSql(email, role) {
@@ -4918,8 +5144,8 @@ async function setUserRole(email, role, options2 = {}) {
4918
5144
  const sql2 = userRoleSql(email, role);
4919
5145
  const roles = await listRoles(options2);
4920
5146
  if (roles && !roles.includes(role)) {
4921
- log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4922
- log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
5147
+ log2(pc23.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
5148
+ log2(pc23.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4923
5149
  return 1;
4924
5150
  }
4925
5151
  const db = readD1Databases(appRoot)[0];
@@ -4933,31 +5159,31 @@ ${result.output.trim()}`);
4933
5159
  const [first] = extractJson(result.output);
4934
5160
  const updated = first?.results ?? [];
4935
5161
  if (updated.length === 0) {
4936
- log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
5162
+ log2(pc23.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4937
5163
  return 1;
4938
5164
  }
4939
- log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
5165
+ log2(pc23.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4940
5166
  return 0;
4941
5167
  }
4942
5168
 
4943
5169
  // src/utils/install.ts
4944
5170
  import { spawn as spawn5 } from "child_process";
4945
- import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
4946
- import { join as join27 } from "path";
5171
+ import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
5172
+ import { join as join28 } from "path";
4947
5173
  function expectedPackages(dir) {
4948
- const npmLock = join27(dir, "package-lock.json");
4949
- if (existsSync25(npmLock)) {
5174
+ const npmLock = join28(dir, "package-lock.json");
5175
+ if (existsSync26(npmLock)) {
4950
5176
  try {
4951
- const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
5177
+ const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
4952
5178
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4953
5179
  } catch {
4954
5180
  return 0;
4955
5181
  }
4956
5182
  }
4957
- const pnpmLock = join27(dir, "pnpm-lock.yaml");
4958
- if (existsSync25(pnpmLock)) {
5183
+ const pnpmLock = join28(dir, "pnpm-lock.yaml");
5184
+ if (existsSync26(pnpmLock)) {
4959
5185
  try {
4960
- const text = readFileSync15(pnpmLock, "utf8");
5186
+ const text = readFileSync17(pnpmLock, "utf8");
4961
5187
  const packages = text.slice(text.indexOf("\npackages:"));
4962
5188
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4963
5189
  } catch {
@@ -4967,15 +5193,15 @@ function expectedPackages(dir) {
4967
5193
  return 0;
4968
5194
  }
4969
5195
  function installedPackages(dir) {
4970
- const root = join27(dir, "node_modules");
4971
- if (!existsSync25(root)) return 0;
5196
+ const root = join28(dir, "node_modules");
5197
+ if (!existsSync26(root)) return 0;
4972
5198
  let total = 0;
4973
5199
  try {
4974
- for (const entry of readdirSync9(root, { withFileTypes: true })) {
5200
+ for (const entry of readdirSync10(root, { withFileTypes: true })) {
4975
5201
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4976
5202
  if (entry.name.startsWith("@")) {
4977
5203
  try {
4978
- total += readdirSync9(join27(root, entry.name)).length;
5204
+ total += readdirSync10(join28(root, entry.name)).length;
4979
5205
  } catch {
4980
5206
  }
4981
5207
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -5022,9 +5248,9 @@ function createCli() {
5022
5248
  cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
5023
5249
  async (dir, options2) => {
5024
5250
  const wanted = !options2.skipInstall;
5025
- let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
5251
+ let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
5026
5252
  if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
5027
- const result = createApp(dir, { pm: options2.pm, stack: options2.stack, install: false, ...answers, log: (message) => prompts3.log.message(message) });
5253
+ const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
5028
5254
  let installed = false;
5029
5255
  if (wanted) {
5030
5256
  const spin = prompts3.spinner({ indicator: "timer" });
@@ -5036,7 +5262,7 @@ function createCli() {
5036
5262
  });
5037
5263
  installed = outcome.code === 0;
5038
5264
  if (installed) {
5039
- spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
5265
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc24.dim(`(${formatDuration(outcome.ms)})`)}`);
5040
5266
  } else {
5041
5267
  spin.error(`${result.packageManager} install failed`);
5042
5268
  process.stderr.write(outcome.output);
@@ -5086,6 +5312,13 @@ function createCli() {
5086
5312
  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) => {
5087
5313
  await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
5088
5314
  });
5315
+ cli.command("diff [filter]", "Show how the code Flare copied into this app (lib/resource) differs from the current version").example("flare diff").example('flare diff store # just the files whose path contains "store"').action((filter) => {
5316
+ diffTracked({ filter });
5317
+ });
5318
+ cli.command("update [filter]", "Replace this app's copies of Flare's files with the current version (discards your edits)").option("-y, --yes", "Confirm overwriting").example("flare update # lists what would change").example("flare update --yes # applies it").action((filter, options2) => {
5319
+ const code = updateTracked({ filter, yes: options2.yes });
5320
+ if (code !== 0) process.exitCode = code;
5321
+ });
5089
5322
  cli.command("db:push [...tables]", "Copy rows from the local database to the deployed one (seeded catalogues, reference data)").option("--truncate", "Delete the remote rows of those tables first").option("--database <binding>", "D1 binding, when the app has several").option("--env <name>", "Wrangler environment").option("-y, --yes", "Confirm writing to the deployed database").example("flare db:push products categories --yes").example("flare db:push plans --truncate --yes").action(async (tables, options2) => {
5090
5323
  await dbPush(tables, options2);
5091
5324
  });
@@ -5134,6 +5367,6 @@ try {
5134
5367
  }
5135
5368
  }
5136
5369
  } catch (error) {
5137
- console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
5370
+ console.error(pc25.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
5138
5371
  process.exitCode = 1;
5139
5372
  }