@flaredev/cli 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.ts
2
- import 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
@@ -285,6 +290,20 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? "
285
290
  const name = userAgent.split("/")[0];
286
291
  return isPackageManager(name) ? name : "pnpm";
287
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
+ }
288
307
  function run(command, args, cwd) {
289
308
  const env = childEnv();
290
309
  const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true, env }) : spawnSync(command, args, { cwd, stdio: "inherit", env });
@@ -304,6 +323,7 @@ function installArgs(pm) {
304
323
  }
305
324
 
306
325
  // src/commands/create.ts
326
+ var ALLOW_BUILDS = ["'@prisma/engines': true", "esbuild: true", "prisma: true", "sharp: false", "workerd: true"].map((line) => ` ${line}`).join("\n");
307
327
  function toAppName(dir) {
308
328
  const name = basename(resolve2(dir)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
309
329
  if (!name) throw new Error(`Cannot derive an app name from "${dir}". Use letters, digits, and dashes.`);
@@ -325,10 +345,7 @@ function createApp(target, options2 = {}) {
325
345
  if (existsSync3(dir) && readdirSync2(dir).length > 0) {
326
346
  throw new Error(`Directory ${dir} already exists and is not empty.`);
327
347
  }
328
- if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
329
- throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
330
- }
331
- const packageManager = options2.pm ?? detectPackageManager();
348
+ const { packageManager, preferred: prefersPnpm } = choosePackageManager(options2.pm);
332
349
  if (options2.stack !== void 0 && !isStack(options2.stack)) {
333
350
  throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
334
351
  }
@@ -395,13 +412,15 @@ MAIL_FROM=
395
412
  ${devVarsEntries(authProviders)}`
396
413
  );
397
414
  if (packageManager === "pnpm" && !inWorkspace) {
398
- const allowBuilds = stack === "next" ? "allowBuilds:\n '@prisma/engines': true\n prisma: true\n sharp: false\n" : "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n";
399
- writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
415
+ writeFileSync3(join3(dir, "pnpm-workspace.yaml"), `allowBuilds:
416
+ ${ALLOW_BUILDS}
417
+ `);
400
418
  }
401
419
  const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
402
420
  if (pinned) copyLockfile(packageManager, dir);
403
421
  const log2 = options2.log ?? ((message) => console.log(message));
404
422
  log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
423
+ if (prefersPnpm) log2(pc.dim(`Installing with pnpm \u2014 far quicker for an app this size. Use ${pc.bold("--pm npm")} to change that.`));
405
424
  return { dir, name, packageManager, inWorkspace };
406
425
  }
407
426
  function copyLockfile(packageManager, dir) {
@@ -1798,12 +1817,14 @@ function resolveBin(appRoot, pkgName, binName2) {
1798
1817
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1799
1818
  return join8(dirname4(manifestPath), relativeBin);
1800
1819
  }
1820
+ var NEXT_COMMANDS = {
1821
+ dev: { pkg: "next", bin: "next", args: ["dev"] },
1822
+ build: { pkg: "next", bin: "next", args: ["build"] },
1823
+ start: { pkg: "next", bin: "next", args: ["start"] },
1824
+ deploy: { pkg: "vercel", bin: "vercel", args: ["deploy", "--prod"] }
1825
+ };
1801
1826
  function delegatedArgv(appRoot, command, forwarded) {
1802
- if (readStack(appRoot) === "next") {
1803
- const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
1804
- throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
1805
- }
1806
- const spec = DELEGATED_COMMANDS[command];
1827
+ const spec = readStack(appRoot) === "next" ? NEXT_COMMANDS[command] : DELEGATED_COMMANDS[command];
1807
1828
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1808
1829
  }
1809
1830
  function runNode(argv, cwd) {
@@ -1858,15 +1879,22 @@ function portArg(args) {
1858
1879
  }
1859
1880
  async function runDelegated(command, forwarded, cwd = process.cwd()) {
1860
1881
  const appRoot = findAppRoot(cwd);
1882
+ const next = readStack(appRoot) === "next";
1861
1883
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1862
1884
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1863
1885
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1864
- if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
1886
+ if (next && command === "build" && !wantsHelp) {
1887
+ const code = await runNode([resolveBin(appRoot, "prisma", "prisma"), "generate"], appRoot);
1888
+ if (code !== 0) return code;
1889
+ }
1890
+ const built = next ? ".next" : "dist/server/wrangler.json";
1891
+ if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, built))) {
1865
1892
  console.log("No production build found; running `flare build` first.\n");
1866
- const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1893
+ const code = await runDelegated("build", [], appRoot);
1867
1894
  if (code !== 0) return code;
1868
1895
  }
1869
1896
  if (command === "deploy") {
1897
+ if (next) return runNode(delegatedArgv(appRoot, "deploy", forwarded), appRoot);
1870
1898
  return runDeploy(forwarded, {
1871
1899
  appRoot,
1872
1900
  wranglerBin: resolveBin(appRoot, "wrangler", "wrangler"),
@@ -1960,7 +1988,7 @@ function previousSnapshot(dir, migrationFile) {
1960
1988
 
1961
1989
  // src/generator/plan.ts
1962
1990
  import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
1963
- import { dirname as dirname5, join as join11 } from "path";
1991
+ import { dirname as dirname5, join as join11, sep } from "path";
1964
1992
  import pc5 from "picocolors";
1965
1993
 
1966
1994
  // src/generator/policy.ts
@@ -2285,14 +2313,19 @@ ${lines.join("\n")}
2285
2313
  var handlerImports = (entry, stack = "cloudflare") => {
2286
2314
  const local = resourceLocal(entry.stem);
2287
2315
  const source = stack === "next" ? {
2288
- imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
2316
+ imports: [`import { createResourceHandlers, prismaRows } from "@/lib/resource";`, `import { prisma } from "@/lib/db";`],
2289
2317
  wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2290
2318
  } : {
2291
- imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2292
- wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
2319
+ imports: [
2320
+ `import { createResourceHandlers, drizzleRows } from "@/lib/resource";`,
2321
+ `import { getDb } from "@/db";`,
2322
+ `import { ${tableExport(entry.resource)} } from "@/db/schema";`
2323
+ ],
2324
+ // Named rather than implied: the adapter is the one thing about this route
2325
+ // that differs between stacks, so it is written out.
2326
+ wiring: [` rows: drizzleRows(${tableExport(entry.resource)}, getDb),`]
2293
2327
  };
2294
2328
  return [
2295
- `import { createResourceHandlers } from "@flaredev/core/server";`,
2296
2329
  ...source.imports,
2297
2330
  `import { authorize, currentUser } from "@/lib/api";`,
2298
2331
  `import { revalidateResource } from "@/lib/cache";`,
@@ -2326,6 +2359,49 @@ function renderItemRoute(entry, stack = "cloudflare") {
2326
2359
  var adminImports = (entry) => [
2327
2360
  `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`
2328
2361
  ];
2362
+ function renderAdminListLoading(entry) {
2363
+ const columns = Math.min(6, storedFields2(entry.resource).length + 1);
2364
+ return [
2365
+ `import { ResourceListSkeleton } from "@/components/dashboard/skeletons";`,
2366
+ "",
2367
+ `export default function Loading() {`,
2368
+ ` return <ResourceListSkeleton columns={${columns}} />;`,
2369
+ "}",
2370
+ ""
2371
+ ].join("\n");
2372
+ }
2373
+ function renderAdminFormLoading(entry) {
2374
+ const fields = Math.min(8, Math.max(1, storedFields2(entry.resource).length));
2375
+ return [
2376
+ `import { FormSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
2377
+ "",
2378
+ `export default function Loading() {`,
2379
+ " return (",
2380
+ " <>",
2381
+ " <PageHeaderSkeleton actions={0} />",
2382
+ ` <FormSkeleton fields={${fields}} />`,
2383
+ " </>",
2384
+ " );",
2385
+ "}",
2386
+ ""
2387
+ ].join("\n");
2388
+ }
2389
+ function renderAdminDetailLoading(entry) {
2390
+ const rows = Math.min(10, Math.max(1, storedFields2(entry.resource).length));
2391
+ return [
2392
+ `import { DetailSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
2393
+ "",
2394
+ `export default function Loading() {`,
2395
+ " return (",
2396
+ " <>",
2397
+ " <PageHeaderSkeleton />",
2398
+ ` <DetailSkeleton rows={${rows}} />`,
2399
+ " </>",
2400
+ " );",
2401
+ "}",
2402
+ ""
2403
+ ].join("\n");
2404
+ }
2329
2405
  function renderAdminListPage(entry) {
2330
2406
  const local = resourceLocal(entry.stem);
2331
2407
  return [
@@ -2468,7 +2544,11 @@ function resourceFiles(entry, all, stack = "cloudflare") {
2468
2544
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2469
2545
  { path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
2470
2546
  { path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2471
- { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
2547
+ { path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) },
2548
+ { path: `app/dashboard/${resource.slug}/loading.tsx`, content: renderAdminListLoading(entry) },
2549
+ { path: `app/dashboard/${resource.slug}/new/loading.tsx`, content: renderAdminFormLoading(entry) },
2550
+ { path: `app/dashboard/${resource.slug}/[id]/loading.tsx`, content: renderAdminDetailLoading(entry) },
2551
+ { path: `app/dashboard/${resource.slug}/[id]/edit/loading.tsx`, content: renderAdminFormLoading(entry) }
2472
2552
  ];
2473
2553
  }
2474
2554
 
@@ -2550,7 +2630,9 @@ function findOrphans(appRoot, all) {
2550
2630
  for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2551
2631
  }
2552
2632
  for (const dir of list2("app/dashboard", () => true)) {
2553
- for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
2633
+ const pages = [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`];
2634
+ const skeletons = [`${dir}/loading.tsx`, `${dir}/new/loading.tsx`, `${dir}/[id]/loading.tsx`, `${dir}/[id]/edit/loading.tsx`];
2635
+ for (const file of [...pages, ...skeletons]) {
2554
2636
  if (existsSync12(join11(appRoot, file))) candidates.push(file);
2555
2637
  }
2556
2638
  }
@@ -2562,11 +2644,19 @@ function findOrphans(appRoot, all) {
2562
2644
  return orphans;
2563
2645
  }
2564
2646
  function ensureSupportFiles(appRoot, log2) {
2565
- for (const path of ["lib/api.ts", "lib/cache.ts"]) {
2647
+ const stack = readStack(appRoot);
2648
+ const engine = ["rows.ts", "query.ts", "store.ts", "handlers.ts", "index.ts", stack === "next" ? "prisma-rows.ts" : "drizzle-rows.ts"].map(
2649
+ (name) => `lib/resource/${name}`
2650
+ );
2651
+ for (const path of ["lib/api.ts", "lib/cache.ts", ...engine]) {
2566
2652
  const target = join11(appRoot, path);
2567
2653
  if (existsSync12(target)) continue;
2654
+ const source = [join11(templatesDir, "next", path), join11(templatesDir, "app", path)].find(
2655
+ (candidate) => (stack === "next" || !candidate.includes(`${sep}next${sep}`)) && existsSync12(candidate)
2656
+ );
2657
+ if (!source) continue;
2568
2658
  mkdirSync4(dirname5(target), { recursive: true });
2569
- copyFileSync2(join11(templatesDir, "app", path), target);
2659
+ copyFileSync2(source, target);
2570
2660
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2571
2661
  }
2572
2662
  }
@@ -2610,30 +2700,30 @@ function addStripeDependency(appRoot) {
2610
2700
  return true;
2611
2701
  }
2612
2702
  function writeBillingResource(appRoot, name, force, log2) {
2613
- const relative2 = descriptorPath(name);
2614
- const path = join12(appRoot, relative2);
2703
+ const relative3 = descriptorPath(name);
2704
+ const path = join12(appRoot, relative3);
2615
2705
  if (!existsSync13(path)) {
2616
2706
  mkdirSync5(dirname6(path), { recursive: true });
2617
2707
  writeFileSync7(path, renderBillingDescriptor(name));
2618
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2708
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2619
2709
  return;
2620
2710
  }
2621
2711
  const parts = splitMarkers(readFileSync9(path, "utf8"));
2622
- 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]}`);
2712
+ 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]}`);
2623
2713
  const merged = mergeBillingFields(name, parts.block);
2624
2714
  if (!merged) {
2625
- log2(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2715
+ log2(`${pc6.dim("identical".padEnd(9))} ${relative3} (has every billing field)`);
2626
2716
  return;
2627
2717
  }
2628
2718
  try {
2629
2719
  writeGenerated(path, merged, { force });
2630
2720
  } catch (error) {
2631
2721
  if (error instanceof DriftError) {
2632
- 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]}`);
2722
+ 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]}`);
2633
2723
  }
2634
2724
  throw error;
2635
2725
  }
2636
- log2(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2726
+ log2(`${pc6.yellow("update".padEnd(9))} ${relative3} (added billing fields, kept yours)`);
2637
2727
  }
2638
2728
  async function genBilling(options2 = {}) {
2639
2729
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -2641,14 +2731,14 @@ async function genBilling(options2 = {}) {
2641
2731
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2642
2732
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2643
2733
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2644
- const relative2 = policyPath(name);
2645
- if (existsSync13(join12(appRoot, relative2))) {
2646
- log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2734
+ const relative3 = policyPath(name);
2735
+ if (existsSync13(join12(appRoot, relative3))) {
2736
+ log2(`${pc6.dim("kept".padEnd(9))} ${relative3} (already exists)`);
2647
2737
  continue;
2648
2738
  }
2649
2739
  mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2650
- writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2651
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2740
+ writeFileSync7(join12(appRoot, relative3), renderPolicy(name, roles));
2741
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2652
2742
  }
2653
2743
  const files = [];
2654
2744
  const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
@@ -3233,15 +3323,15 @@ function securityFiles(appName) {
3233
3323
  ];
3234
3324
  }
3235
3325
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
3236
- function writeOnce(appRoot, relative2, content, log2) {
3237
- const path = join13(appRoot, relative2);
3326
+ function writeOnce(appRoot, relative3, content, log2) {
3327
+ const path = join13(appRoot, relative3);
3238
3328
  if (existsSync14(path)) {
3239
- log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
3329
+ log2(`${pc7.dim("kept".padEnd(9))} ${relative3} (already exists)`);
3240
3330
  return;
3241
3331
  }
3242
3332
  mkdirSync6(dirname7(path), { recursive: true });
3243
3333
  writeFileSync8(path, content);
3244
- log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
3334
+ log2(`${pc7.green("create".padEnd(9))} ${relative3}`);
3245
3335
  }
3246
3336
  async function genSecurity(options2 = {}) {
3247
3337
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -3513,16 +3603,16 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3513
3603
  const action = options2.action ?? (method === "GET" ? "read" : "update");
3514
3604
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3515
3605
  const { resource, stem } = entry;
3516
- const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3517
- const path = join16(appRoot, relative2);
3518
- if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
3606
+ const relative3 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
3607
+ const path = join16(appRoot, relative3);
3608
+ if (existsSync17(path)) throw new Error(`${relative3} already exists.`);
3519
3609
  mkdirSync8(join16(path, ".."), { recursive: true });
3520
3610
  const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3521
3611
  writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3522
- log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3612
+ log2(`${pc11.green("create".padEnd(9))} ${relative3}`);
3523
3613
  log2(`
3524
3614
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
3525
- return relative2;
3615
+ return relative3;
3526
3616
  }
3527
3617
  function renderEndpoint({ resource, stem, method, action, record, route }) {
3528
3618
  const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
@@ -3612,8 +3702,8 @@ async function genPolicy(rawName, options2) {
3612
3702
  const known = resources.map(({ resource }) => resource.name).join(", ") || "none";
3613
3703
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3614
3704
  }
3615
- const relative2 = policyPath(name);
3616
- const path = join18(appRoot, relative2);
3705
+ const relative3 = policyPath(name);
3706
+ const path = join18(appRoot, relative3);
3617
3707
  const exists = existsSync19(path);
3618
3708
  let roles;
3619
3709
  let deleteRoles;
@@ -3627,14 +3717,14 @@ async function genPolicy(rawName, options2) {
3627
3717
  if (!exists) {
3628
3718
  mkdirSync10(dirname9(path), { recursive: true });
3629
3719
  writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3630
- log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3720
+ log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
3631
3721
  } else {
3632
3722
  try {
3633
3723
  const outcome = writeGenerated(path, block, { force: options2.force });
3634
- log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative2}`);
3724
+ log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative3}`);
3635
3725
  } catch (error) {
3636
3726
  if (error instanceof DriftError) {
3637
- throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
3727
+ throw new Error(`The roles in ${relative3} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
3638
3728
  }
3639
3729
  throw error;
3640
3730
  }
@@ -3644,7 +3734,7 @@ async function genPolicy(rawName, options2) {
3644
3734
  logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3645
3735
  log2(`
3646
3736
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3647
- return { name, path: relative2 };
3737
+ return { name, path: relative3 };
3648
3738
  }
3649
3739
 
3650
3740
  // src/commands/migrate.ts
@@ -3993,12 +4083,118 @@ ${result.output.trim()}`);
3993
4083
  return 0;
3994
4084
  }
3995
4085
 
4086
+ // src/commands/eject.ts
4087
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync15 } from "fs";
4088
+ import { dirname as dirname11, join as join21, relative as relative2 } from "path";
4089
+ import pc17 from "picocolors";
4090
+ var TRACKED = ["lib/resource"];
4091
+ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only) => path === only || path.startsWith(`${only}/`));
4092
+ function upstreamFile(path, stack) {
4093
+ if (prunedOn(path, stack)) return void 0;
4094
+ const candidates = stack === "next" ? [join21(templatesDir, "next", path), join21(templatesDir, "app", path)] : [join21(templatesDir, "app", path)];
4095
+ return candidates.find((candidate) => existsSync22(candidate));
4096
+ }
4097
+ function trackedPaths(appRoot, stack) {
4098
+ const paths = /* @__PURE__ */ new Set();
4099
+ const walk = (base, dir) => {
4100
+ if (!existsSync22(join21(base, dir))) return;
4101
+ for (const entry of readdirSync8(join21(base, dir), { withFileTypes: true })) {
4102
+ const next = `${dir}/${entry.name}`;
4103
+ if (entry.isDirectory()) walk(base, next);
4104
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
4105
+ }
4106
+ };
4107
+ for (const root of TRACKED) {
4108
+ walk(appRoot, root);
4109
+ walk(join21(templatesDir, "app"), root);
4110
+ if (stack === "next") walk(join21(templatesDir, "next"), root);
4111
+ }
4112
+ return [...paths].filter((path) => upstreamFile(path, stack) !== void 0).sort();
4113
+ }
4114
+ function compareTracked(appRoot, stack, filter) {
4115
+ return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
4116
+ const source = upstreamFile(path, stack);
4117
+ const theirs = readFileSync14(source, "utf8");
4118
+ const target = join21(appRoot, path);
4119
+ if (!existsSync22(target)) return { path, state: "missing", theirs };
4120
+ const mine = readFileSync14(target, "utf8");
4121
+ return { path, state: mine === theirs ? "same" : "changed", mine, theirs };
4122
+ });
4123
+ }
4124
+ function summarise(mine, theirs) {
4125
+ const a = mine.split("\n");
4126
+ const b = theirs.split("\n");
4127
+ const lines = [];
4128
+ const max = Math.max(a.length, b.length);
4129
+ let shown = 0;
4130
+ for (let index = 0; index < max && shown < 12; index += 1) {
4131
+ if (a[index] === b[index]) continue;
4132
+ if (a[index] !== void 0) lines.push(pc17.red(` - ${a[index].slice(0, 100)}`));
4133
+ if (b[index] !== void 0) lines.push(pc17.green(` + ${b[index].slice(0, 100)}`));
4134
+ shown += 1;
4135
+ }
4136
+ return lines;
4137
+ }
4138
+ function diffTracked(options2 = {}) {
4139
+ const log2 = options2.log ?? ((message) => console.log(message));
4140
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4141
+ const stack = readStack(appRoot);
4142
+ const comparisons = compareTracked(appRoot, stack, options2.filter);
4143
+ const changed = comparisons.filter((entry) => entry.state !== "same");
4144
+ if (changed.length === 0) {
4145
+ log2(pc17.green(`Up to date: ${comparisons.length} file(s) match this version of Flare.`));
4146
+ return comparisons;
4147
+ }
4148
+ log2(pc17.bold(`${changed.length} of ${comparisons.length} file(s) differ from this version of Flare:
4149
+ `));
4150
+ for (const entry of changed) {
4151
+ if (entry.state === "missing") {
4152
+ log2(`${pc17.yellow("missing")} ${entry.path} ${pc17.dim("(Flare ships this; your app doesn't have it)")}`);
4153
+ continue;
4154
+ }
4155
+ log2(`${pc17.cyan("changed")} ${entry.path}`);
4156
+ for (const line of summarise(entry.mine, entry.theirs)) log2(line);
4157
+ log2("");
4158
+ }
4159
+ log2(pc17.dim(`Yours is "-", Flare's is "+". Apply with ${pc17.bold("flare update")}, or keep yours and do nothing.`));
4160
+ return comparisons;
4161
+ }
4162
+ function updateTracked(options2 = {}) {
4163
+ const log2 = options2.log ?? ((message) => console.log(message));
4164
+ const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4165
+ const stack = readStack(appRoot);
4166
+ const comparisons = compareTracked(appRoot, stack, options2.filter);
4167
+ const changed = comparisons.filter((entry) => entry.state !== "same");
4168
+ if (changed.length === 0) {
4169
+ log2(pc17.green("Nothing to update."));
4170
+ return 0;
4171
+ }
4172
+ if (!options2.yes) {
4173
+ log2(pc17.bold(`${changed.length} file(s) would be overwritten with Flare's version:
4174
+ `));
4175
+ for (const entry of changed) log2(` ${entry.path}`);
4176
+ log2(pc17.yellow(`
4177
+ This discards your edits to those files. Re-run with ${pc17.bold("--yes")} to proceed.`));
4178
+ log2(pc17.dim(`See what would change first with ${pc17.bold("flare diff")}.`));
4179
+ return 1;
4180
+ }
4181
+ for (const entry of changed) {
4182
+ const target = join21(appRoot, entry.path);
4183
+ mkdirSync11(dirname11(target), { recursive: true });
4184
+ writeFileSync15(target, entry.theirs);
4185
+ log2(`${pc17.green("update")} ${relative2(process.cwd(), target) || entry.path}`);
4186
+ }
4187
+ log2(pc17.green(`
4188
+ Updated ${changed.length} file(s).`));
4189
+ return 0;
4190
+ }
4191
+
3996
4192
  // src/commands/seed.ts
3997
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
3998
- import { join as join22 } from "path";
4193
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
4194
+ import { join as join23 } from "path";
3999
4195
  import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
4000
4196
  import { createJiti as createJiti4 } from "jiti";
4001
- import pc17 from "picocolors";
4197
+ import pc18 from "picocolors";
4002
4198
 
4003
4199
  // src/seed/bulk.ts
4004
4200
  var MAX_STATEMENT_BYTES = 8e4;
@@ -4092,17 +4288,20 @@ function createInsertMany(sink, drizzle, onProgress) {
4092
4288
  }
4093
4289
 
4094
4290
  // src/seed/local-d1.ts
4291
+ import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4095
4292
  import { createRequire } from "module";
4096
- import { join as join21 } from "path";
4293
+ import { join as join22 } from "path";
4097
4294
  import { pathToFileURL } from "url";
4295
+ import { parse as parseJsonc3 } from "jsonc-parser";
4098
4296
  var LOCAL_STATE = ".wrangler/state/v3";
4099
4297
  async function openLocalD1(appRoot, binding) {
4100
4298
  process.env.WRANGLER_LOG ??= "error";
4101
- const appRequire = createRequire(join21(appRoot, "package.json"));
4299
+ const appRequire = createRequire(join22(appRoot, "package.json"));
4102
4300
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
4301
+ const configPath = seedConfig(appRoot);
4103
4302
  const { env, dispose } = await wrangler.getPlatformProxy({
4104
- configPath: join21(appRoot, "wrangler.jsonc"),
4105
- persist: { path: join21(appRoot, LOCAL_STATE) }
4303
+ configPath,
4304
+ persist: { path: join22(appRoot, LOCAL_STATE) }
4106
4305
  });
4107
4306
  const bindings = env;
4108
4307
  const name = binding ?? "DB";
@@ -4111,14 +4310,30 @@ async function openLocalD1(appRoot, binding) {
4111
4310
  const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
4112
4311
  throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
4113
4312
  }
4114
- return { db, env: bindings, dispose };
4313
+ return {
4314
+ db,
4315
+ env: bindings,
4316
+ dispose: async () => {
4317
+ await dispose();
4318
+ rmSync4(configPath, { force: true });
4319
+ }
4320
+ };
4321
+ }
4322
+ function seedConfig(appRoot) {
4323
+ const source = join22(appRoot, "wrangler.jsonc");
4324
+ const config = parseJsonc3(readFileSync15(source, "utf8")) ?? {};
4325
+ delete config.durable_objects;
4326
+ delete config.migrations;
4327
+ const path = join22(appRoot, ".flare-seed.wrangler.json");
4328
+ writeFileSync16(path, JSON.stringify(config, null, 2));
4329
+ return path;
4115
4330
  }
4116
4331
 
4117
4332
  // src/commands/seed.ts
4118
4333
  var SEEDS_DIR = "seeds";
4119
4334
  function seedFiles(appRoot, names) {
4120
- const dir = join22(appRoot, SEEDS_DIR);
4121
- const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4335
+ const dir = join23(appRoot, SEEDS_DIR);
4336
+ const all = existsSync23(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4122
4337
  if (!names?.length) return all;
4123
4338
  return names.map((name) => {
4124
4339
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -4133,28 +4348,28 @@ async function runSeeds(options2 = {}) {
4133
4348
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4134
4349
  const files = seedFiles(appRoot, options2.names);
4135
4350
  if (files.length === 0) {
4136
- log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
4351
+ log2(`No seeds found. Create one with ${pc18.bold("flare seed:make <name>")}.`);
4137
4352
  return;
4138
4353
  }
4139
4354
  if (readStack(appRoot) === "next") {
4140
4355
  await runPrismaSeeds(appRoot, files, log2);
4141
4356
  return;
4142
4357
  }
4143
- const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4358
+ const jiti = createJiti4(join23(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
4144
4359
  const { drizzle } = await jiti.import("drizzle-orm/d1");
4145
4360
  const drizzleHelpers = await jiti.import("drizzle-orm");
4146
- const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
4361
+ const schema = await jiti.import(join23(appRoot, "db/schema.ts"));
4147
4362
  const local = await openLocalD1(appRoot);
4148
4363
  try {
4149
4364
  const db = drizzle(local.db, { schema });
4150
4365
  const insertMany = createInsertMany(local.db, drizzleHelpers);
4151
4366
  for (const file of files) {
4152
- const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
4367
+ const mod = await jiti.import(join23(appRoot, SEEDS_DIR, file));
4153
4368
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
4154
- log2(`${pc17.cyan("seed")} ${file}`);
4369
+ log2(`${pc18.cyan("seed")} ${file}`);
4155
4370
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
4156
4371
  }
4157
- log2(pc17.green(`
4372
+ log2(pc18.green(`
4158
4373
  Ran ${files.length} seed(s) against the local database.`));
4159
4374
  } finally {
4160
4375
  await local.dispose();
@@ -4265,9 +4480,9 @@ async function makeSeed(rawName, options2 = {}) {
4265
4480
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4266
4481
  const name = kebabCase4(rawName);
4267
4482
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
4268
- const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
4269
- const path = join22(appRoot, relative2);
4270
- if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
4483
+ const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
4484
+ const path = join23(appRoot, relative3);
4485
+ if (existsSync23(path)) throw new Error(`${relative3} already exists.`);
4271
4486
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4272
4487
  let resource;
4273
4488
  if (options2.resource) {
@@ -4276,15 +4491,15 @@ async function makeSeed(rawName, options2 = {}) {
4276
4491
  } else {
4277
4492
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
4278
4493
  }
4279
- mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4280
- writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
4281
- log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
4494
+ mkdirSync12(join23(appRoot, SEEDS_DIR), { recursive: true });
4495
+ writeFileSync17(path, renderSeed(resource, readStack(appRoot)));
4496
+ log2(`${pc18.green("create".padEnd(9))} ${relative3}`);
4282
4497
  log2(`
4283
- Run it with ${pc17.bold(`flare seed ${name}`)}.`);
4284
- return relative2;
4498
+ Run it with ${pc18.bold(`flare seed ${name}`)}.`);
4499
+ return relative3;
4285
4500
  }
4286
4501
  async function runPrismaSeeds(appRoot, files, log2) {
4287
- const runner = join22(appRoot, ".flare-seed-runner.mts");
4502
+ const runner = join23(appRoot, ".flare-seed-runner.mts");
4288
4503
  const script = [
4289
4504
  `import "dotenv/config";`,
4290
4505
  `import { createFake } from "@flaredev/core";`,
@@ -4312,23 +4527,23 @@ async function runPrismaSeeds(appRoot, files, log2) {
4312
4527
  `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4313
4528
  ""
4314
4529
  ].join("\n");
4315
- writeFileSync15(runner, script);
4530
+ writeFileSync17(runner, script);
4316
4531
  try {
4317
4532
  const tsx = resolveBin(appRoot, "tsx", "tsx");
4318
4533
  const code = await runNode([tsx, runner], appRoot);
4319
4534
  if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4320
- log2(pc17.green(`
4535
+ log2(pc18.green(`
4321
4536
  Ran ${files.length} seed(s).`));
4322
4537
  } finally {
4323
- rmSync4(runner, { force: true });
4538
+ rmSync5(runner, { force: true });
4324
4539
  }
4325
4540
  }
4326
4541
 
4327
4542
  // src/commands/db-push.ts
4328
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
4543
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
4329
4544
  import { tmpdir as tmpdir2 } from "os";
4330
- import { join as join23 } from "path";
4331
- import pc18 from "picocolors";
4545
+ import { join as join24 } from "path";
4546
+ import pc19 from "picocolors";
4332
4547
 
4333
4548
  // src/terminal.ts
4334
4549
  var isTTY = () => Boolean(process.stdout.isTTY);
@@ -4392,7 +4607,7 @@ async function dbPush(tables, options2 = {}) {
4392
4607
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4393
4608
  const started = Date.now();
4394
4609
  const local = await openLocalD1(appRoot, options2.database);
4395
- const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4610
+ const dir = mkdtempSync2(join24(tmpdir2(), "flare-push-"));
4396
4611
  try {
4397
4612
  const statements = [];
4398
4613
  const counts = [];
@@ -4411,33 +4626,33 @@ async function dbPush(tables, options2 = {}) {
4411
4626
  offset += PAGE;
4412
4627
  }
4413
4628
  counts.push({ table, rows });
4414
- log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
4629
+ log2(`${pc19.cyan("read")} ${table} ${pc19.dim(`(${formatCount(rows)} rows)`)}`);
4415
4630
  }
4416
4631
  const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
4417
4632
  if (total === 0) {
4418
4633
  log2("Nothing to push: those tables are empty locally.");
4419
4634
  return { tables: counts, ms: Date.now() - started };
4420
4635
  }
4421
- const file = join23(dir, "push.sql");
4422
- writeFileSync16(file, statements.join("\n"));
4636
+ const file = join24(dir, "push.sql");
4637
+ writeFileSync18(file, statements.join("\n"));
4423
4638
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4424
4639
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4425
4640
  if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
4426
4641
  ${result.output.trim()}`);
4427
4642
  const ms = Date.now() - started;
4428
- log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
4643
+ log2(`${pc19.green("\u2714")} Pushed ${pc19.bold(formatCount(total))} rows to ${pc19.bold(db.database_name)} ${pc19.dim(`(${formatDuration(ms)})`)}`);
4429
4644
  return { tables: counts, ms };
4430
4645
  } finally {
4431
- rmSync5(dir, { recursive: true, force: true });
4646
+ rmSync6(dir, { recursive: true, force: true });
4432
4647
  await local.dispose();
4433
4648
  }
4434
4649
  }
4435
4650
 
4436
4651
  // src/commands/seed-resource.ts
4437
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
4652
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
4438
4653
  import { tmpdir as tmpdir3 } from "os";
4439
- import { join as join24 } from "path";
4440
- import pc19 from "picocolors";
4654
+ import { join as join25 } from "path";
4655
+ import pc20 from "picocolors";
4441
4656
 
4442
4657
  // src/seed/rows.ts
4443
4658
  import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
@@ -4621,13 +4836,13 @@ async function seedResourceCommand(name, options2 = {}) {
4621
4836
  ...options2,
4622
4837
  onProgress: (written, total) => {
4623
4838
  const elapsed = Date.now() - started;
4624
- progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
4839
+ progress.update(` ${pc20.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc20.dim(formatRate(written, elapsed))}`);
4625
4840
  }
4626
4841
  });
4627
4842
  progress.done();
4628
4843
  const { rows, ms, where } = result;
4629
4844
  console.log(
4630
- `${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4845
+ `${pc20.green("\u2714")} Seeded ${pc20.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc20.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4631
4846
  );
4632
4847
  return result;
4633
4848
  } catch (error) {
@@ -4675,8 +4890,8 @@ ${result.output.trim()}`);
4675
4890
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4676
4891
  startIndex = counted?.total ?? 0;
4677
4892
  }
4678
- const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4679
- const file = join24(dir, `${resource.table}.sql`);
4893
+ const dir = mkdtempSync3(join25(tmpdir3(), "flare-seed-"));
4894
+ const file = join25(dir, `${resource.table}.sql`);
4680
4895
  try {
4681
4896
  const statements = [];
4682
4897
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4686,13 +4901,13 @@ ${result.output.trim()}`);
4686
4901
  written += statement.rows;
4687
4902
  options2.onProgress?.(written, total);
4688
4903
  }
4689
- writeFileSync17(file, statements.join("\n"));
4690
- log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
4904
+ writeFileSync19(file, statements.join("\n"));
4905
+ log2(`${pc20.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc20.bold(db.database_name)}`);
4691
4906
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4692
4907
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4693
4908
  ${result.output.trim()}`);
4694
4909
  } finally {
4695
- rmSync6(dir, { recursive: true, force: true });
4910
+ rmSync7(dir, { recursive: true, force: true });
4696
4911
  }
4697
4912
  return { resource, rows: total, ms: Date.now() - started, where };
4698
4913
  }
@@ -4719,17 +4934,17 @@ ${result.output.trim()}`);
4719
4934
 
4720
4935
  // src/commands/billing-sync.ts
4721
4936
  import { createRequire as createRequire2 } from "module";
4722
- import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
4937
+ import { existsSync as existsSync24, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
4723
4938
  import { tmpdir as tmpdir4 } from "os";
4724
- import { join as join25 } from "path";
4939
+ import { join as join26 } from "path";
4725
4940
  import { pathToFileURL as pathToFileURL2 } from "url";
4726
4941
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4727
- import pc20 from "picocolors";
4942
+ import pc21 from "picocolors";
4728
4943
  function stripeKeys(appRoot) {
4729
4944
  const map = {};
4730
- const path = join25(appRoot, ".dev.vars");
4731
- if (existsSync23(path)) {
4732
- for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
4945
+ const path = join26(appRoot, ".dev.vars");
4946
+ if (existsSync24(path)) {
4947
+ for (const line of readFileSync16(path, "utf8").split(/\r?\n/)) {
4733
4948
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4734
4949
  if (match) map[match[1]] = match[2].trim();
4735
4950
  }
@@ -4812,10 +5027,10 @@ async function syncPlans(options2 = {}) {
4812
5027
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4813
5028
  const db = readD1Databases(appRoot)[0];
4814
5029
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4815
- const appRequire = createRequire2(join25(appRoot, "package.json"));
5030
+ const appRequire = createRequire2(join26(appRoot, "package.json"));
4816
5031
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4817
5032
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4818
- const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
5033
+ const appName = JSON.parse(readFileSync16(join26(appRoot, "package.json"), "utf8")).name ?? "app";
4819
5034
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4820
5035
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4821
5036
  if (products.length === 0) {
@@ -4836,41 +5051,41 @@ async function syncPlans(options2 = {}) {
4836
5051
  ${existingResult.output.trim()}`);
4837
5052
  }
4838
5053
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4839
- const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
5054
+ const dir = mkdtempSync4(join26(tmpdir4(), "flare-sync-"));
4840
5055
  try {
4841
- const file = join25(dir, "sync-plans.sql");
4842
- writeFileSync18(file, renderSyncSql(rows));
5056
+ const file = join26(dir, "sync-plans.sql");
5057
+ writeFileSync20(file, renderSyncSql(rows));
4843
5058
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4844
5059
  if (result.code !== 0) throw new Error(`Could not write plans:
4845
5060
  ${result.output.trim()}`);
4846
5061
  } finally {
4847
- rmSync7(dir, { recursive: true, force: true });
5062
+ rmSync8(dir, { recursive: true, force: true });
4848
5063
  }
4849
5064
  let created = 0;
4850
5065
  for (const row of rows) {
4851
5066
  const isNew = !known.has(row.stripePriceId);
4852
5067
  if (isNew) created++;
4853
5068
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
4854
- log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
5069
+ log2(`${(isNew ? pc21.green("create") : pc21.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc21.dim(price)}`);
4855
5070
  }
4856
- for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
5071
+ for (const name of skipped) log2(pc21.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
4857
5072
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
4858
- 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})`);
4859
- log2(pc20.green(`
5073
+ 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})`);
5074
+ log2(pc21.green(`
4860
5075
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
4861
5076
  return { created, updated: rows.length - created, skipped };
4862
5077
  }
4863
5078
 
4864
5079
  // src/commands/sync.ts
4865
- import { existsSync as existsSync24 } from "fs";
4866
- import { join as join26 } from "path";
4867
- import pc21 from "picocolors";
5080
+ import { existsSync as existsSync25 } from "fs";
5081
+ import { join as join27 } from "path";
5082
+ import pc22 from "picocolors";
4868
5083
  async function syncTypes(options2 = {}) {
4869
5084
  const log2 = options2.log ?? ((message) => console.log(message));
4870
5085
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4871
5086
  const all = await loadResources(appRoot);
4872
5087
  const policies = await loadPolicies(appRoot);
4873
- const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
5088
+ const tracksPolicies = policies.length > 0 || existsSync25(join27(appRoot, "policies/index.ts"));
4874
5089
  const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4875
5090
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4876
5091
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -4882,42 +5097,42 @@ async function syncTypes(options2 = {}) {
4882
5097
  if (drift.length) {
4883
5098
  log2(
4884
5099
  `
4885
- ${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
5100
+ ${pc22.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
4886
5101
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
4887
5102
  );
4888
5103
  }
4889
5104
  if (unmarked.length) {
4890
5105
  log2(`
4891
- ${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
5106
+ ${pc22.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
4892
5107
  }
4893
5108
  if (orphans.length) {
4894
5109
  const resources = [...new Set(orphans.map((o) => o.resource))];
4895
5110
  log2(`
4896
- ${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
5111
+ ${pc22.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
4897
5112
  for (const orphan of orphans) log2(` ${orphan.path}`);
4898
5113
  log2(
4899
- `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.`
5114
+ `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.`
4900
5115
  );
4901
5116
  }
4902
5117
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
4903
5118
  if (tablesChanged && !options2.check) {
4904
5119
  log2(`
4905
- ${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
5120
+ ${pc22.cyan("Tables changed.")} Create a migration with ${pc22.bold("flare gen migration <name> --from-schema")}, then ${pc22.bold("flare migrate")}.`);
4906
5121
  }
4907
5122
  const problems = drift.length + unmarked.length + orphans.length;
4908
5123
  if (options2.check) {
4909
5124
  const stale = changed.length;
4910
- if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
5125
+ if (stale === 0 && problems === 0) log2(pc22.green("Generated files are in sync with the resource descriptors."));
4911
5126
  else log2(`
4912
- ${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
5127
+ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flare sync-types")}.`);
4913
5128
  return stale + problems > 0 ? 1 : 0;
4914
5129
  }
4915
- if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
5130
+ if (changed.length === 0 && problems === 0) log2(pc22.green("Everything is in sync."));
4916
5131
  return problems > 0 ? 1 : 0;
4917
5132
  }
4918
5133
 
4919
5134
  // src/commands/user-role.ts
4920
- import pc22 from "picocolors";
5135
+ import pc23 from "picocolors";
4921
5136
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
4922
5137
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
4923
5138
  function userRoleSql(email, role) {
@@ -4931,8 +5146,8 @@ async function setUserRole(email, role, options2 = {}) {
4931
5146
  const sql2 = userRoleSql(email, role);
4932
5147
  const roles = await listRoles(options2);
4933
5148
  if (roles && !roles.includes(role)) {
4934
- log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
4935
- log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
5149
+ log2(pc23.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
5150
+ log2(pc23.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
4936
5151
  return 1;
4937
5152
  }
4938
5153
  const db = readD1Databases(appRoot)[0];
@@ -4946,31 +5161,31 @@ ${result.output.trim()}`);
4946
5161
  const [first] = extractJson(result.output);
4947
5162
  const updated = first?.results ?? [];
4948
5163
  if (updated.length === 0) {
4949
- log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
5164
+ log2(pc23.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4950
5165
  return 1;
4951
5166
  }
4952
- log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
5167
+ log2(pc23.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
4953
5168
  return 0;
4954
5169
  }
4955
5170
 
4956
5171
  // src/utils/install.ts
4957
5172
  import { spawn as spawn5 } from "child_process";
4958
- import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
4959
- import { join as join27 } from "path";
5173
+ import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
5174
+ import { join as join28 } from "path";
4960
5175
  function expectedPackages(dir) {
4961
- const npmLock = join27(dir, "package-lock.json");
4962
- if (existsSync25(npmLock)) {
5176
+ const npmLock = join28(dir, "package-lock.json");
5177
+ if (existsSync26(npmLock)) {
4963
5178
  try {
4964
- const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
5179
+ const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
4965
5180
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4966
5181
  } catch {
4967
5182
  return 0;
4968
5183
  }
4969
5184
  }
4970
- const pnpmLock = join27(dir, "pnpm-lock.yaml");
4971
- if (existsSync25(pnpmLock)) {
5185
+ const pnpmLock = join28(dir, "pnpm-lock.yaml");
5186
+ if (existsSync26(pnpmLock)) {
4972
5187
  try {
4973
- const text = readFileSync15(pnpmLock, "utf8");
5188
+ const text = readFileSync17(pnpmLock, "utf8");
4974
5189
  const packages = text.slice(text.indexOf("\npackages:"));
4975
5190
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4976
5191
  } catch {
@@ -4980,15 +5195,15 @@ function expectedPackages(dir) {
4980
5195
  return 0;
4981
5196
  }
4982
5197
  function installedPackages(dir) {
4983
- const root = join27(dir, "node_modules");
4984
- if (!existsSync25(root)) return 0;
5198
+ const root = join28(dir, "node_modules");
5199
+ if (!existsSync26(root)) return 0;
4985
5200
  let total = 0;
4986
5201
  try {
4987
- for (const entry of readdirSync9(root, { withFileTypes: true })) {
5202
+ for (const entry of readdirSync10(root, { withFileTypes: true })) {
4988
5203
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4989
5204
  if (entry.name.startsWith("@")) {
4990
5205
  try {
4991
- total += readdirSync9(join27(root, entry.name)).length;
5206
+ total += readdirSync10(join28(root, entry.name)).length;
4992
5207
  } catch {
4993
5208
  }
4994
5209
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -5049,7 +5264,7 @@ function createCli() {
5049
5264
  });
5050
5265
  installed = outcome.code === 0;
5051
5266
  if (installed) {
5052
- spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
5267
+ spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc24.dim(`(${formatDuration(outcome.ms)})`)}`);
5053
5268
  } else {
5054
5269
  spin.error(`${result.packageManager} install failed`);
5055
5270
  process.stderr.write(outcome.output);
@@ -5099,6 +5314,13 @@ function createCli() {
5099
5314
  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) => {
5100
5315
  await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
5101
5316
  });
5317
+ 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) => {
5318
+ diffTracked({ filter });
5319
+ });
5320
+ 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) => {
5321
+ const code = updateTracked({ filter, yes: options2.yes });
5322
+ if (code !== 0) process.exitCode = code;
5323
+ });
5102
5324
  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) => {
5103
5325
  await dbPush(tables, options2);
5104
5326
  });
@@ -5147,6 +5369,6 @@ try {
5147
5369
  }
5148
5370
  }
5149
5371
  } catch (error) {
5150
- console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
5372
+ console.error(pc25.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
5151
5373
  process.exitCode = 1;
5152
5374
  }