@flaredev/cli 0.5.1 → 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
@@ -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 });
@@ -325,10 +344,7 @@ function createApp(target, options2 = {}) {
325
344
  if (existsSync3(dir) && readdirSync2(dir).length > 0) {
326
345
  throw new Error(`Directory ${dir} already exists and is not empty.`);
327
346
  }
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();
347
+ const { packageManager, preferred: prefersPnpm } = choosePackageManager(options2.pm);
332
348
  if (options2.stack !== void 0 && !isStack(options2.stack)) {
333
349
  throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
334
350
  }
@@ -402,6 +418,7 @@ ${devVarsEntries(authProviders)}`
402
418
  if (pinned) copyLockfile(packageManager, dir);
403
419
  const log2 = options2.log ?? ((message) => console.log(message));
404
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.`));
405
422
  return { dir, name, packageManager, inWorkspace };
406
423
  }
407
424
  function copyLockfile(packageManager, dir) {
@@ -1798,12 +1815,14 @@ function resolveBin(appRoot, pkgName, binName2) {
1798
1815
  if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1799
1816
  return join8(dirname4(manifestPath), relativeBin);
1800
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
+ };
1801
1824
  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];
1825
+ const spec = readStack(appRoot) === "next" ? NEXT_COMMANDS[command] : DELEGATED_COMMANDS[command];
1807
1826
  return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
1808
1827
  }
1809
1828
  function runNode(argv, cwd) {
@@ -1858,15 +1877,22 @@ function portArg(args) {
1858
1877
  }
1859
1878
  async function runDelegated(command, forwarded, cwd = process.cwd()) {
1860
1879
  const appRoot = findAppRoot(cwd);
1880
+ const next = readStack(appRoot) === "next";
1861
1881
  const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1862
1882
  if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1863
1883
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1864
- 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))) {
1865
1890
  console.log("No production build found; running `flare build` first.\n");
1866
- const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1891
+ const code = await runDelegated("build", [], appRoot);
1867
1892
  if (code !== 0) return code;
1868
1893
  }
1869
1894
  if (command === "deploy") {
1895
+ if (next) return runNode(delegatedArgv(appRoot, "deploy", forwarded), appRoot);
1870
1896
  return runDeploy(forwarded, {
1871
1897
  appRoot,
1872
1898
  wranglerBin: resolveBin(appRoot, "wrangler", "wrangler"),
@@ -1960,7 +1986,7 @@ function previousSnapshot(dir, migrationFile) {
1960
1986
 
1961
1987
  // src/generator/plan.ts
1962
1988
  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";
1989
+ import { dirname as dirname5, join as join11, sep } from "path";
1964
1990
  import pc5 from "picocolors";
1965
1991
 
1966
1992
  // src/generator/policy.ts
@@ -2285,14 +2311,19 @@ ${lines.join("\n")}
2285
2311
  var handlerImports = (entry, stack = "cloudflare") => {
2286
2312
  const local = resourceLocal(entry.stem);
2287
2313
  const source = stack === "next" ? {
2288
- 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";`],
2289
2315
  wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
2290
2316
  } : {
2291
- imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
2292
- 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),`]
2293
2325
  };
2294
2326
  return [
2295
- `import { createResourceHandlers } from "@flaredev/core/server";`,
2296
2327
  ...source.imports,
2297
2328
  `import { authorize, currentUser } from "@/lib/api";`,
2298
2329
  `import { revalidateResource } from "@/lib/cache";`,
@@ -2326,6 +2357,49 @@ function renderItemRoute(entry, stack = "cloudflare") {
2326
2357
  var adminImports = (entry) => [
2327
2358
  `import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`
2328
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
+ }
2329
2403
  function renderAdminListPage(entry) {
2330
2404
  const local = resourceLocal(entry.stem);
2331
2405
  return [
@@ -2468,7 +2542,11 @@ function resourceFiles(entry, all, stack = "cloudflare") {
2468
2542
  { path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
2469
2543
  { path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
2470
2544
  { path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
2471
- { 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) }
2472
2550
  ];
2473
2551
  }
2474
2552
 
@@ -2550,7 +2628,9 @@ function findOrphans(appRoot, all) {
2550
2628
  for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
2551
2629
  }
2552
2630
  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`]) {
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]) {
2554
2634
  if (existsSync12(join11(appRoot, file))) candidates.push(file);
2555
2635
  }
2556
2636
  }
@@ -2562,11 +2642,19 @@ function findOrphans(appRoot, all) {
2562
2642
  return orphans;
2563
2643
  }
2564
2644
  function ensureSupportFiles(appRoot, log2) {
2565
- 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]) {
2566
2650
  const target = join11(appRoot, path);
2567
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;
2568
2656
  mkdirSync4(dirname5(target), { recursive: true });
2569
- copyFileSync2(join11(templatesDir, "app", path), target);
2657
+ copyFileSync2(source, target);
2570
2658
  log2(`${pc5.green("create".padEnd(9))} ${path}`);
2571
2659
  }
2572
2660
  }
@@ -2610,30 +2698,30 @@ function addStripeDependency(appRoot) {
2610
2698
  return true;
2611
2699
  }
2612
2700
  function writeBillingResource(appRoot, name, force, log2) {
2613
- const relative2 = descriptorPath(name);
2614
- const path = join12(appRoot, relative2);
2701
+ const relative3 = descriptorPath(name);
2702
+ const path = join12(appRoot, relative3);
2615
2703
  if (!existsSync13(path)) {
2616
2704
  mkdirSync5(dirname6(path), { recursive: true });
2617
2705
  writeFileSync7(path, renderBillingDescriptor(name));
2618
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2706
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2619
2707
  return;
2620
2708
  }
2621
2709
  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]}`);
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]}`);
2623
2711
  const merged = mergeBillingFields(name, parts.block);
2624
2712
  if (!merged) {
2625
- log2(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2713
+ log2(`${pc6.dim("identical".padEnd(9))} ${relative3} (has every billing field)`);
2626
2714
  return;
2627
2715
  }
2628
2716
  try {
2629
2717
  writeGenerated(path, merged, { force });
2630
2718
  } catch (error) {
2631
2719
  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]}`);
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]}`);
2633
2721
  }
2634
2722
  throw error;
2635
2723
  }
2636
- 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)`);
2637
2725
  }
2638
2726
  async function genBilling(options2 = {}) {
2639
2727
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -2641,14 +2729,14 @@ async function genBilling(options2 = {}) {
2641
2729
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2642
2730
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
2643
2731
  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)`);
2732
+ const relative3 = policyPath(name);
2733
+ if (existsSync13(join12(appRoot, relative3))) {
2734
+ log2(`${pc6.dim("kept".padEnd(9))} ${relative3} (already exists)`);
2647
2735
  continue;
2648
2736
  }
2649
2737
  mkdirSync5(join12(appRoot, "policies"), { recursive: true });
2650
- writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
2651
- log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
2738
+ writeFileSync7(join12(appRoot, relative3), renderPolicy(name, roles));
2739
+ log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
2652
2740
  }
2653
2741
  const files = [];
2654
2742
  const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
@@ -3233,15 +3321,15 @@ function securityFiles(appName) {
3233
3321
  ];
3234
3322
  }
3235
3323
  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);
3324
+ function writeOnce(appRoot, relative3, content, log2) {
3325
+ const path = join13(appRoot, relative3);
3238
3326
  if (existsSync14(path)) {
3239
- log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
3327
+ log2(`${pc7.dim("kept".padEnd(9))} ${relative3} (already exists)`);
3240
3328
  return;
3241
3329
  }
3242
3330
  mkdirSync6(dirname7(path), { recursive: true });
3243
3331
  writeFileSync8(path, content);
3244
- log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
3332
+ log2(`${pc7.green("create".padEnd(9))} ${relative3}`);
3245
3333
  }
3246
3334
  async function genSecurity(options2 = {}) {
3247
3335
  const log2 = options2.log ?? ((message) => console.log(message));
@@ -3513,16 +3601,16 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
3513
3601
  const action = options2.action ?? (method === "GET" ? "read" : "update");
3514
3602
  if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
3515
3603
  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.`);
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.`);
3519
3607
  mkdirSync8(join16(path, ".."), { recursive: true });
3520
3608
  const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
3521
3609
  writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
3522
- log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
3610
+ log2(`${pc11.green("create".padEnd(9))} ${relative3}`);
3523
3611
  log2(`
3524
3612
  It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
3525
- return relative2;
3613
+ return relative3;
3526
3614
  }
3527
3615
  function renderEndpoint({ resource, stem, method, action, record, route }) {
3528
3616
  const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
@@ -3612,8 +3700,8 @@ async function genPolicy(rawName, options2) {
3612
3700
  const known = resources.map(({ resource }) => resource.name).join(", ") || "none";
3613
3701
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3614
3702
  }
3615
- const relative2 = policyPath(name);
3616
- const path = join18(appRoot, relative2);
3703
+ const relative3 = policyPath(name);
3704
+ const path = join18(appRoot, relative3);
3617
3705
  const exists = existsSync19(path);
3618
3706
  let roles;
3619
3707
  let deleteRoles;
@@ -3627,14 +3715,14 @@ async function genPolicy(rawName, options2) {
3627
3715
  if (!exists) {
3628
3716
  mkdirSync10(dirname9(path), { recursive: true });
3629
3717
  writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3630
- log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
3718
+ log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
3631
3719
  } else {
3632
3720
  try {
3633
3721
  const outcome = writeGenerated(path, block, { force: options2.force });
3634
- log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative2}`);
3722
+ log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative3}`);
3635
3723
  } catch (error) {
3636
3724
  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.`);
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.`);
3638
3726
  }
3639
3727
  throw error;
3640
3728
  }
@@ -3644,7 +3732,7 @@ async function genPolicy(rawName, options2) {
3644
3732
  logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
3645
3733
  log2(`
3646
3734
  ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3647
- return { name, path: relative2 };
3735
+ return { name, path: relative3 };
3648
3736
  }
3649
3737
 
3650
3738
  // src/commands/migrate.ts
@@ -3993,12 +4081,118 @@ ${result.output.trim()}`);
3993
4081
  return 0;
3994
4082
  }
3995
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
+
3996
4190
  // 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";
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";
3999
4193
  import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
4000
4194
  import { createJiti as createJiti4 } from "jiti";
4001
- import pc17 from "picocolors";
4195
+ import pc18 from "picocolors";
4002
4196
 
4003
4197
  // src/seed/bulk.ts
4004
4198
  var MAX_STATEMENT_BYTES = 8e4;
@@ -4092,17 +4286,20 @@ function createInsertMany(sink, drizzle, onProgress) {
4092
4286
  }
4093
4287
 
4094
4288
  // src/seed/local-d1.ts
4289
+ import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
4095
4290
  import { createRequire } from "module";
4096
- import { join as join21 } from "path";
4291
+ import { join as join22 } from "path";
4097
4292
  import { pathToFileURL } from "url";
4293
+ import { parse as parseJsonc3 } from "jsonc-parser";
4098
4294
  var LOCAL_STATE = ".wrangler/state/v3";
4099
4295
  async function openLocalD1(appRoot, binding) {
4100
4296
  process.env.WRANGLER_LOG ??= "error";
4101
- const appRequire = createRequire(join21(appRoot, "package.json"));
4297
+ const appRequire = createRequire(join22(appRoot, "package.json"));
4102
4298
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
4299
+ const configPath = seedConfig(appRoot);
4103
4300
  const { env, dispose } = await wrangler.getPlatformProxy({
4104
- configPath: join21(appRoot, "wrangler.jsonc"),
4105
- persist: { path: join21(appRoot, LOCAL_STATE) }
4301
+ configPath,
4302
+ persist: { path: join22(appRoot, LOCAL_STATE) }
4106
4303
  });
4107
4304
  const bindings = env;
4108
4305
  const name = binding ?? "DB";
@@ -4111,14 +4308,30 @@ async function openLocalD1(appRoot, binding) {
4111
4308
  const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
4112
4309
  throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
4113
4310
  }
4114
- 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;
4115
4328
  }
4116
4329
 
4117
4330
  // src/commands/seed.ts
4118
4331
  var SEEDS_DIR = "seeds";
4119
4332
  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() : [];
4333
+ const dir = join23(appRoot, SEEDS_DIR);
4334
+ const all = existsSync23(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
4122
4335
  if (!names?.length) return all;
4123
4336
  return names.map((name) => {
4124
4337
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -4133,28 +4346,28 @@ async function runSeeds(options2 = {}) {
4133
4346
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4134
4347
  const files = seedFiles(appRoot, options2.names);
4135
4348
  if (files.length === 0) {
4136
- 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>")}.`);
4137
4350
  return;
4138
4351
  }
4139
4352
  if (readStack(appRoot) === "next") {
4140
4353
  await runPrismaSeeds(appRoot, files, log2);
4141
4354
  return;
4142
4355
  }
4143
- 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 });
4144
4357
  const { drizzle } = await jiti.import("drizzle-orm/d1");
4145
4358
  const drizzleHelpers = await jiti.import("drizzle-orm");
4146
- const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
4359
+ const schema = await jiti.import(join23(appRoot, "db/schema.ts"));
4147
4360
  const local = await openLocalD1(appRoot);
4148
4361
  try {
4149
4362
  const db = drizzle(local.db, { schema });
4150
4363
  const insertMany = createInsertMany(local.db, drizzleHelpers);
4151
4364
  for (const file of files) {
4152
- const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
4365
+ const mod = await jiti.import(join23(appRoot, SEEDS_DIR, file));
4153
4366
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
4154
- log2(`${pc17.cyan("seed")} ${file}`);
4367
+ log2(`${pc18.cyan("seed")} ${file}`);
4155
4368
  await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
4156
4369
  }
4157
- log2(pc17.green(`
4370
+ log2(pc18.green(`
4158
4371
  Ran ${files.length} seed(s) against the local database.`));
4159
4372
  } finally {
4160
4373
  await local.dispose();
@@ -4265,9 +4478,9 @@ async function makeSeed(rawName, options2 = {}) {
4265
4478
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4266
4479
  const name = kebabCase4(rawName);
4267
4480
  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.`);
4481
+ const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
4482
+ const path = join23(appRoot, relative3);
4483
+ if (existsSync23(path)) throw new Error(`${relative3} already exists.`);
4271
4484
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
4272
4485
  let resource;
4273
4486
  if (options2.resource) {
@@ -4276,15 +4489,15 @@ async function makeSeed(rawName, options2 = {}) {
4276
4489
  } else {
4277
4490
  resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
4278
4491
  }
4279
- mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
4280
- writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
4281
- 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}`);
4282
4495
  log2(`
4283
- Run it with ${pc17.bold(`flare seed ${name}`)}.`);
4284
- return relative2;
4496
+ Run it with ${pc18.bold(`flare seed ${name}`)}.`);
4497
+ return relative3;
4285
4498
  }
4286
4499
  async function runPrismaSeeds(appRoot, files, log2) {
4287
- const runner = join22(appRoot, ".flare-seed-runner.mts");
4500
+ const runner = join23(appRoot, ".flare-seed-runner.mts");
4288
4501
  const script = [
4289
4502
  `import "dotenv/config";`,
4290
4503
  `import { createFake } from "@flaredev/core";`,
@@ -4312,23 +4525,23 @@ async function runPrismaSeeds(appRoot, files, log2) {
4312
4525
  `await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
4313
4526
  ""
4314
4527
  ].join("\n");
4315
- writeFileSync15(runner, script);
4528
+ writeFileSync17(runner, script);
4316
4529
  try {
4317
4530
  const tsx = resolveBin(appRoot, "tsx", "tsx");
4318
4531
  const code = await runNode([tsx, runner], appRoot);
4319
4532
  if (code !== 0) throw new Error("A seed failed; nothing further was run.");
4320
- log2(pc17.green(`
4533
+ log2(pc18.green(`
4321
4534
  Ran ${files.length} seed(s).`));
4322
4535
  } finally {
4323
- rmSync4(runner, { force: true });
4536
+ rmSync5(runner, { force: true });
4324
4537
  }
4325
4538
  }
4326
4539
 
4327
4540
  // src/commands/db-push.ts
4328
- 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";
4329
4542
  import { tmpdir as tmpdir2 } from "os";
4330
- import { join as join23 } from "path";
4331
- import pc18 from "picocolors";
4543
+ import { join as join24 } from "path";
4544
+ import pc19 from "picocolors";
4332
4545
 
4333
4546
  // src/terminal.ts
4334
4547
  var isTTY = () => Boolean(process.stdout.isTTY);
@@ -4392,7 +4605,7 @@ async function dbPush(tables, options2 = {}) {
4392
4605
  const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
4393
4606
  const started = Date.now();
4394
4607
  const local = await openLocalD1(appRoot, options2.database);
4395
- const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
4608
+ const dir = mkdtempSync2(join24(tmpdir2(), "flare-push-"));
4396
4609
  try {
4397
4610
  const statements = [];
4398
4611
  const counts = [];
@@ -4411,33 +4624,33 @@ async function dbPush(tables, options2 = {}) {
4411
4624
  offset += PAGE;
4412
4625
  }
4413
4626
  counts.push({ table, rows });
4414
- log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
4627
+ log2(`${pc19.cyan("read")} ${table} ${pc19.dim(`(${formatCount(rows)} rows)`)}`);
4415
4628
  }
4416
4629
  const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
4417
4630
  if (total === 0) {
4418
4631
  log2("Nothing to push: those tables are empty locally.");
4419
4632
  return { tables: counts, ms: Date.now() - started };
4420
4633
  }
4421
- const file = join23(dir, "push.sql");
4422
- writeFileSync16(file, statements.join("\n"));
4634
+ const file = join24(dir, "push.sql");
4635
+ writeFileSync18(file, statements.join("\n"));
4423
4636
  const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
4424
4637
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4425
4638
  if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
4426
4639
  ${result.output.trim()}`);
4427
4640
  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)})`)}`);
4641
+ log2(`${pc19.green("\u2714")} Pushed ${pc19.bold(formatCount(total))} rows to ${pc19.bold(db.database_name)} ${pc19.dim(`(${formatDuration(ms)})`)}`);
4429
4642
  return { tables: counts, ms };
4430
4643
  } finally {
4431
- rmSync5(dir, { recursive: true, force: true });
4644
+ rmSync6(dir, { recursive: true, force: true });
4432
4645
  await local.dispose();
4433
4646
  }
4434
4647
  }
4435
4648
 
4436
4649
  // src/commands/seed-resource.ts
4437
- 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";
4438
4651
  import { tmpdir as tmpdir3 } from "os";
4439
- import { join as join24 } from "path";
4440
- import pc19 from "picocolors";
4652
+ import { join as join25 } from "path";
4653
+ import pc20 from "picocolors";
4441
4654
 
4442
4655
  // src/seed/rows.ts
4443
4656
  import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
@@ -4621,13 +4834,13 @@ async function seedResourceCommand(name, options2 = {}) {
4621
4834
  ...options2,
4622
4835
  onProgress: (written, total) => {
4623
4836
  const elapsed = Date.now() - started;
4624
- 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))}`);
4625
4838
  }
4626
4839
  });
4627
4840
  progress.done();
4628
4841
  const { rows, ms, where } = result;
4629
4842
  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)})`)
4843
+ `${pc20.green("\u2714")} Seeded ${pc20.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc20.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
4631
4844
  );
4632
4845
  return result;
4633
4846
  } catch (error) {
@@ -4675,8 +4888,8 @@ ${result.output.trim()}`);
4675
4888
  const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
4676
4889
  startIndex = counted?.total ?? 0;
4677
4890
  }
4678
- const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
4679
- const file = join24(dir, `${resource.table}.sql`);
4891
+ const dir = mkdtempSync3(join25(tmpdir3(), "flare-seed-"));
4892
+ const file = join25(dir, `${resource.table}.sql`);
4680
4893
  try {
4681
4894
  const statements = [];
4682
4895
  if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
@@ -4686,13 +4899,13 @@ ${result.output.trim()}`);
4686
4899
  written += statement.rows;
4687
4900
  options2.onProgress?.(written, total);
4688
4901
  }
4689
- writeFileSync17(file, statements.join("\n"));
4690
- 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)}`);
4691
4904
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
4692
4905
  if (result.code !== 0) throw new Error(`Seeding the remote database failed:
4693
4906
  ${result.output.trim()}`);
4694
4907
  } finally {
4695
- rmSync6(dir, { recursive: true, force: true });
4908
+ rmSync7(dir, { recursive: true, force: true });
4696
4909
  }
4697
4910
  return { resource, rows: total, ms: Date.now() - started, where };
4698
4911
  }
@@ -4719,17 +4932,17 @@ ${result.output.trim()}`);
4719
4932
 
4720
4933
  // src/commands/billing-sync.ts
4721
4934
  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";
4935
+ import { existsSync as existsSync24, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
4723
4936
  import { tmpdir as tmpdir4 } from "os";
4724
- import { join as join25 } from "path";
4937
+ import { join as join26 } from "path";
4725
4938
  import { pathToFileURL as pathToFileURL2 } from "url";
4726
4939
  import { kebabCase as kebabCase5 } from "@flaredev/core";
4727
- import pc20 from "picocolors";
4940
+ import pc21 from "picocolors";
4728
4941
  function stripeKeys(appRoot) {
4729
4942
  const map = {};
4730
- const path = join25(appRoot, ".dev.vars");
4731
- if (existsSync23(path)) {
4732
- 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/)) {
4733
4946
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
4734
4947
  if (match) map[match[1]] = match[2].trim();
4735
4948
  }
@@ -4812,10 +5025,10 @@ async function syncPlans(options2 = {}) {
4812
5025
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
4813
5026
  const db = readD1Databases(appRoot)[0];
4814
5027
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
4815
- const appRequire = createRequire2(join25(appRoot, "package.json"));
5028
+ const appRequire = createRequire2(join26(appRoot, "package.json"));
4816
5029
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
4817
5030
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
4818
- 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";
4819
5032
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
4820
5033
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
4821
5034
  if (products.length === 0) {
@@ -4836,41 +5049,41 @@ async function syncPlans(options2 = {}) {
4836
5049
  ${existingResult.output.trim()}`);
4837
5050
  }
4838
5051
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
4839
- const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
5052
+ const dir = mkdtempSync4(join26(tmpdir4(), "flare-sync-"));
4840
5053
  try {
4841
- const file = join25(dir, "sync-plans.sql");
4842
- writeFileSync18(file, renderSyncSql(rows));
5054
+ const file = join26(dir, "sync-plans.sql");
5055
+ writeFileSync20(file, renderSyncSql(rows));
4843
5056
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
4844
5057
  if (result.code !== 0) throw new Error(`Could not write plans:
4845
5058
  ${result.output.trim()}`);
4846
5059
  } finally {
4847
- rmSync7(dir, { recursive: true, force: true });
5060
+ rmSync8(dir, { recursive: true, force: true });
4848
5061
  }
4849
5062
  let created = 0;
4850
5063
  for (const row of rows) {
4851
5064
  const isNew = !known.has(row.stripePriceId);
4852
5065
  if (isNew) created++;
4853
5066
  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)}`);
5067
+ log2(`${(isNew ? pc21.green("create") : pc21.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc21.dim(price)}`);
4855
5068
  }
4856
- 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`));
4857
5070
  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(`
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(`
4860
5073
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
4861
5074
  return { created, updated: rows.length - created, skipped };
4862
5075
  }
4863
5076
 
4864
5077
  // src/commands/sync.ts
4865
- import { existsSync as existsSync24 } from "fs";
4866
- import { join as join26 } from "path";
4867
- import pc21 from "picocolors";
5078
+ import { existsSync as existsSync25 } from "fs";
5079
+ import { join as join27 } from "path";
5080
+ import pc22 from "picocolors";
4868
5081
  async function syncTypes(options2 = {}) {
4869
5082
  const log2 = options2.log ?? ((message) => console.log(message));
4870
5083
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
4871
5084
  const all = await loadResources(appRoot);
4872
5085
  const policies = await loadPolicies(appRoot);
4873
- const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
5086
+ const tracksPolicies = policies.length > 0 || existsSync25(join27(appRoot, "policies/index.ts"));
4874
5087
  const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
4875
5088
  if (!options2.check) ensureSupportFiles(appRoot, log2);
4876
5089
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -4882,42 +5095,42 @@ async function syncTypes(options2 = {}) {
4882
5095
  if (drift.length) {
4883
5096
  log2(
4884
5097
  `
4885
- ${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"}.
4886
5099
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
4887
5100
  );
4888
5101
  }
4889
5102
  if (unmarked.length) {
4890
5103
  log2(`
4891
- ${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.`);
4892
5105
  }
4893
5106
  if (orphans.length) {
4894
5107
  const resources = [...new Set(orphans.map((o) => o.resource))];
4895
5108
  log2(`
4896
- ${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(", ")}):`);
4897
5110
  for (const orphan of orphans) log2(` ${orphan.path}`);
4898
5111
  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.`
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.`
4900
5113
  );
4901
5114
  }
4902
5115
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
4903
5116
  if (tablesChanged && !options2.check) {
4904
5117
  log2(`
4905
- ${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")}.`);
4906
5119
  }
4907
5120
  const problems = drift.length + unmarked.length + orphans.length;
4908
5121
  if (options2.check) {
4909
5122
  const stale = changed.length;
4910
- 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."));
4911
5124
  else log2(`
4912
- ${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")}.`);
4913
5126
  return stale + problems > 0 ? 1 : 0;
4914
5127
  }
4915
- 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."));
4916
5129
  return problems > 0 ? 1 : 0;
4917
5130
  }
4918
5131
 
4919
5132
  // src/commands/user-role.ts
4920
- import pc22 from "picocolors";
5133
+ import pc23 from "picocolors";
4921
5134
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
4922
5135
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
4923
5136
  function userRoleSql(email, role) {
@@ -4931,8 +5144,8 @@ async function setUserRole(email, role, options2 = {}) {
4931
5144
  const sql2 = userRoleSql(email, role);
4932
5145
  const roles = await listRoles(options2);
4933
5146
  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" : ""}`));
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" : ""}`));
4936
5149
  return 1;
4937
5150
  }
4938
5151
  const db = readD1Databases(appRoot)[0];
@@ -4946,31 +5159,31 @@ ${result.output.trim()}`);
4946
5159
  const [first] = extractJson(result.output);
4947
5160
  const updated = first?.results ?? [];
4948
5161
  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.`));
5162
+ log2(pc23.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
4950
5163
  return 1;
4951
5164
  }
4952
- 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"}).`));
4953
5166
  return 0;
4954
5167
  }
4955
5168
 
4956
5169
  // src/utils/install.ts
4957
5170
  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";
5171
+ import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
5172
+ import { join as join28 } from "path";
4960
5173
  function expectedPackages(dir) {
4961
- const npmLock = join27(dir, "package-lock.json");
4962
- if (existsSync25(npmLock)) {
5174
+ const npmLock = join28(dir, "package-lock.json");
5175
+ if (existsSync26(npmLock)) {
4963
5176
  try {
4964
- const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
5177
+ const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
4965
5178
  return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
4966
5179
  } catch {
4967
5180
  return 0;
4968
5181
  }
4969
5182
  }
4970
- const pnpmLock = join27(dir, "pnpm-lock.yaml");
4971
- if (existsSync25(pnpmLock)) {
5183
+ const pnpmLock = join28(dir, "pnpm-lock.yaml");
5184
+ if (existsSync26(pnpmLock)) {
4972
5185
  try {
4973
- const text = readFileSync15(pnpmLock, "utf8");
5186
+ const text = readFileSync17(pnpmLock, "utf8");
4974
5187
  const packages = text.slice(text.indexOf("\npackages:"));
4975
5188
  return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
4976
5189
  } catch {
@@ -4980,15 +5193,15 @@ function expectedPackages(dir) {
4980
5193
  return 0;
4981
5194
  }
4982
5195
  function installedPackages(dir) {
4983
- const root = join27(dir, "node_modules");
4984
- if (!existsSync25(root)) return 0;
5196
+ const root = join28(dir, "node_modules");
5197
+ if (!existsSync26(root)) return 0;
4985
5198
  let total = 0;
4986
5199
  try {
4987
- for (const entry of readdirSync9(root, { withFileTypes: true })) {
5200
+ for (const entry of readdirSync10(root, { withFileTypes: true })) {
4988
5201
  if (!entry.isDirectory() || entry.name === ".bin") continue;
4989
5202
  if (entry.name.startsWith("@")) {
4990
5203
  try {
4991
- total += readdirSync9(join27(root, entry.name)).length;
5204
+ total += readdirSync10(join28(root, entry.name)).length;
4992
5205
  } catch {
4993
5206
  }
4994
5207
  } else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
@@ -5049,7 +5262,7 @@ function createCli() {
5049
5262
  });
5050
5263
  installed = outcome.code === 0;
5051
5264
  if (installed) {
5052
- 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)})`)}`);
5053
5266
  } else {
5054
5267
  spin.error(`${result.packageManager} install failed`);
5055
5268
  process.stderr.write(outcome.output);
@@ -5099,6 +5312,13 @@ function createCli() {
5099
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) => {
5100
5313
  await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
5101
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
+ });
5102
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) => {
5103
5323
  await dbPush(tables, options2);
5104
5324
  });
@@ -5147,6 +5367,6 @@ try {
5147
5367
  }
5148
5368
  }
5149
5369
  } catch (error) {
5150
- 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)}`));
5151
5371
  process.exitCode = 1;
5152
5372
  }