@flaredev/cli 0.1.0 → 0.1.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,5 +1,5 @@
1
1
  // src/index.ts
2
- import pc16 from "picocolors";
2
+ import pc18 from "picocolors";
3
3
 
4
4
  // src/cli.ts
5
5
  import { cac } from "cac";
@@ -242,9 +242,9 @@ ${pc.green("\u2714")} Created ${pc.bold(result.name)} in ${rel}
242
242
  }
243
243
 
244
244
  // src/commands/gen-billing.ts
245
- import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
246
- import { dirname as dirname6, join as join10 } from "path";
247
- import pc4 from "picocolors";
245
+ import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
246
+ import { dirname as dirname6, join as join11 } from "path";
247
+ import pc6 from "picocolors";
248
248
 
249
249
  // src/generator/grammar.ts
250
250
  import { camelCase, FILE_CATEGORIES } from "@flaredev/core";
@@ -1231,31 +1231,146 @@ async function loadResources(root) {
1231
1231
  }
1232
1232
 
1233
1233
  // src/generator/migrations.ts
1234
- import { spawn as spawn3 } from "child_process";
1235
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1236
- import { join as join7 } from "path";
1234
+ import { spawn as spawn4 } from "child_process";
1235
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1236
+ import { join as join8 } from "path";
1237
1237
 
1238
1238
  // src/commands/run.ts
1239
- import { spawn as spawn2 } from "child_process";
1240
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1241
- import { dirname as dirname4, join as join6, resolve as resolve4 } from "path";
1239
+ import { spawn as spawn3 } from "child_process";
1240
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1241
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
1242
+ import pc4 from "picocolors";
1243
+
1244
+ // src/tunnel.ts
1245
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
1246
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
1247
+ import { homedir } from "os";
1248
+ import { join as join4 } from "path";
1249
+ import pc2 from "picocolors";
1250
+ var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
1251
+ var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
1252
+ function parseTunnelUrl(output) {
1253
+ return TUNNEL_URL.exec(output)?.[0] ?? null;
1254
+ }
1255
+ function parseLocalUrl(output) {
1256
+ const match = LOCAL_URL.exec(output.replace(/\x1b\[[0-9;]*m/g, ""));
1257
+ return match ? match[0].replace("0.0.0.0", "localhost") : null;
1258
+ }
1259
+ function toLocalUrl(target) {
1260
+ if (!target) return "http://localhost:3000";
1261
+ if (/^\d+$/.test(target.replace(/^:/, ""))) return `http://localhost:${target.replace(/^:/, "")}`;
1262
+ if (/^https?:\/\//.test(target)) return target;
1263
+ throw new Error(`"${target}" isn't a port or URL. Try: flare tunnel 3000`);
1264
+ }
1265
+ function releaseAsset(platform = process.platform, arch = process.arch) {
1266
+ const cpu = { x64: "amd64", arm64: "arm64", ia32: "386", arm: "arm" }[arch];
1267
+ if (platform === "win32" && (cpu === "amd64" || cpu === "386")) return `cloudflared-windows-${cpu}.exe`;
1268
+ if (platform === "linux" && cpu) return `cloudflared-linux-${cpu}`;
1269
+ if (platform === "darwin" && (cpu === "amd64" || cpu === "arm64")) return `cloudflared-darwin-${cpu}.tgz`;
1270
+ throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
1271
+ }
1272
+ var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
1273
+ var cacheDir = () => join4(homedir(), ".flare", "bin");
1274
+ function onPath() {
1275
+ const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1276
+ return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
1277
+ }
1278
+ async function download(log) {
1279
+ const asset = releaseAsset();
1280
+ const dir = cacheDir();
1281
+ mkdirSync3(dir, { recursive: true });
1282
+ const target = join4(dir, binName);
1283
+ log(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1284
+ const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1285
+ if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1286
+ const bytes = Buffer.from(await response.arrayBuffer());
1287
+ const partial = `${target}.download`;
1288
+ if (asset.endsWith(".tgz")) {
1289
+ writeFileSync4(`${partial}.tgz`, bytes);
1290
+ const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
1291
+ rmSync(`${partial}.tgz`, { force: true });
1292
+ if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
1293
+ } else {
1294
+ writeFileSync4(partial, bytes);
1295
+ renameSync(partial, target);
1296
+ }
1297
+ if (process.platform !== "win32") chmodSync(target, 493);
1298
+ return target;
1299
+ }
1300
+ async function ensureCloudflared(log = console.log) {
1301
+ const found = onPath();
1302
+ if (found) return found;
1303
+ const cached = join4(cacheDir(), binName);
1304
+ if (existsSync5(cached)) return cached;
1305
+ return download(log);
1306
+ }
1307
+ async function openTunnel(localUrl, options2 = {}) {
1308
+ const bin = await ensureCloudflared(options2.log);
1309
+ const child = spawn(bin, ["tunnel", "--no-autoupdate", "--url", localUrl], { stdio: ["ignore", "pipe", "pipe"] });
1310
+ const close = () => {
1311
+ if (child.exitCode === null) child.kill();
1312
+ };
1313
+ process.once("exit", close);
1314
+ const url = await new Promise((resolve6, reject) => {
1315
+ let output = "";
1316
+ const timer = setTimeout(() => {
1317
+ close();
1318
+ reject(new Error(`cloudflared didn't report a tunnel URL within ${(options2.timeoutMs ?? 3e4) / 1e3}s:
1319
+ ${output.slice(-2e3)}`));
1320
+ }, options2.timeoutMs ?? 3e4);
1321
+ const scan = (chunk) => {
1322
+ output += chunk.toString();
1323
+ const found = parseTunnelUrl(output);
1324
+ if (found) {
1325
+ clearTimeout(timer);
1326
+ resolve6(found);
1327
+ }
1328
+ };
1329
+ child.stdout?.on("data", scan);
1330
+ child.stderr?.on("data", scan);
1331
+ child.on("error", (error) => {
1332
+ clearTimeout(timer);
1333
+ reject(error);
1334
+ });
1335
+ child.on("exit", (code) => {
1336
+ clearTimeout(timer);
1337
+ reject(new Error(`cloudflared exited (code ${code}) before the tunnel was ready:
1338
+ ${output.slice(-2e3)}`));
1339
+ });
1340
+ });
1341
+ child.stdout?.resume();
1342
+ child.stderr?.resume();
1343
+ return { url, process: child, close };
1344
+ }
1345
+ function tunnelBanner(publicUrl, localUrl) {
1346
+ const line = "\u2500".repeat(Math.max(publicUrl.length, localUrl.length) + 14);
1347
+ return [
1348
+ "",
1349
+ pc2.cyan(line),
1350
+ ` ${pc2.bold("Public URL")} ${pc2.green(pc2.bold(publicUrl))}`,
1351
+ ` ${pc2.dim("Forwarding")} ${pc2.dim(localUrl)}`,
1352
+ pc2.cyan(line),
1353
+ pc2.dim(" Anyone with this link can reach your local server. Stop with Ctrl+C."),
1354
+ ""
1355
+ ].join("\n");
1356
+ }
1242
1357
 
1243
1358
  // src/commands/deploy.ts
1244
1359
  import { randomBytes as randomBytes2 } from "crypto";
1245
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1246
- import { join as join5 } from "path";
1360
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1361
+ import { join as join6 } from "path";
1247
1362
  import { createZoneClient } from "@flaredev/core/security";
1248
1363
  import { createJiti as createJiti2 } from "jiti";
1249
- import pc2 from "picocolors";
1364
+ import pc3 from "picocolors";
1250
1365
 
1251
1366
  // src/utils/wrangler.ts
1252
- import { spawn } from "child_process";
1253
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
1254
- import { join as join4 } from "path";
1367
+ import { spawn as spawn2 } from "child_process";
1368
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1369
+ import { join as join5 } from "path";
1255
1370
  import { parse as parseJsonc } from "jsonc-parser";
1256
1371
  function runWrangler(wranglerBin, args, cwd, options2 = {}) {
1257
1372
  return new Promise((resolve6, reject) => {
1258
- const child = spawn(process.execPath, [wranglerBin, ...args], {
1373
+ const child = spawn2(process.execPath, [wranglerBin, ...args], {
1259
1374
  cwd,
1260
1375
  stdio: [options2.stdin !== void 0 ? "pipe" : "ignore", options2.capture ? "pipe" : "inherit", options2.capture ? "pipe" : "inherit"]
1261
1376
  });
@@ -1276,8 +1391,8 @@ ${output.trim()}`);
1276
1391
  return JSON.parse(output.slice(start, end + 1));
1277
1392
  }
1278
1393
  function readD1Databases(appRoot) {
1279
- const path = join4(appRoot, "wrangler.jsonc");
1280
- if (!existsSync5(path)) return [];
1394
+ const path = join5(appRoot, "wrangler.jsonc");
1395
+ if (!existsSync6(path)) return [];
1281
1396
  const config = parseJsonc(readFileSync3(path, "utf8"));
1282
1397
  return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
1283
1398
  }
@@ -1317,15 +1432,15 @@ function declaredSecretNames(devVarsExample) {
1317
1432
  return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
1318
1433
  }
1319
1434
  async function loadSecurityConfig(appRoot) {
1320
- const path = join5(appRoot, "security.config.ts");
1321
- if (!existsSync6(path)) return null;
1322
- const jiti = createJiti2(join5(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1435
+ const path = join6(appRoot, "security.config.ts");
1436
+ if (!existsSync7(path)) return null;
1437
+ const jiti = createJiti2(join6(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1323
1438
  const mod = await jiti.import(path);
1324
1439
  if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
1325
1440
  return mod.default;
1326
1441
  }
1327
1442
  async function provisionZoneSecurity(appRoot, config, credentials, options2) {
1328
- const app = JSON.parse(readFileSync4(join5(appRoot, "package.json"), "utf8")).name ?? "app";
1443
+ const app = JSON.parse(readFileSync4(join6(appRoot, "package.json"), "utf8")).name ?? "app";
1329
1444
  const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
1330
1445
  if (config.zone?.rateLimits) {
1331
1446
  const result = await zone.syncRateLimits(config.zone.rateLimits);
@@ -1344,8 +1459,8 @@ async function runDeploy(args, context3) {
1344
1459
  const log = context3.log ?? ((message) => console.log(message));
1345
1460
  const envArgs = plan.env ? ["--env", plan.env] : [];
1346
1461
  const step = (message) => log(`
1347
- ${pc2.bold(pc2.cyan("flare"))} ${message}`);
1348
- const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync6(join5(appRoot, db.migrations_dir ?? "migrations")));
1462
+ ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1463
+ const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1349
1464
  const migrateAfterDeploy = [];
1350
1465
  const migrate2 = async (db) => {
1351
1466
  step(`Applying migrations to ${db.database_name} (remote)`);
@@ -1404,8 +1519,8 @@ ${put.output.trim()}`);
1404
1519
  existing.add(name);
1405
1520
  }
1406
1521
  }
1407
- const examplePath = join5(appRoot, ".dev.vars.example");
1408
- const optional = existsSync6(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1522
+ const examplePath = join6(appRoot, ".dev.vars.example");
1523
+ const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1409
1524
  if (optional.length > 0) {
1410
1525
  step("Optional secrets not set in production (features using them stay off):");
1411
1526
  for (const name of optional) log(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
@@ -1416,10 +1531,10 @@ ${put.output.trim()}`);
1416
1531
 
1417
1532
  // src/commands/run.ts
1418
1533
  var DELEGATED_COMMANDS = {
1419
- dev: { description: "Start the vinext dev server", pkg: "vinext", bin: "vinext", args: ["dev"] },
1534
+ dev: { description: "Start the vinext dev server (--tunnel: also share it on a public URL)", pkg: "vinext", bin: "vinext", args: ["dev"] },
1420
1535
  build: { description: "Build for production (vinext build)", pkg: "vinext", bin: "vinext", args: ["build"] },
1421
1536
  start: {
1422
- description: "Serve the production build locally in workerd (builds first if needed)",
1537
+ description: "Serve the production build locally in workerd, building first if needed (--tunnel: also share it on a public URL)",
1423
1538
  pkg: "wrangler",
1424
1539
  bin: "wrangler",
1425
1540
  // --persist-to shares local D1/R2 state with `flare dev` and local migrations.
@@ -1440,23 +1555,23 @@ function findAppRoot(cwd) {
1440
1555
  while (dir) {
1441
1556
  const root = findUp("package.json", dir);
1442
1557
  if (!root) break;
1443
- const pkg = JSON.parse(readFileSync5(join6(root, "package.json"), "utf8"));
1558
+ const pkg = JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
1444
1559
  if (pkg.dependencies?.vinext || pkg.devDependencies?.vinext) return root;
1445
1560
  const parent = dirname4(root);
1446
1561
  dir = parent === root ? void 0 : parent;
1447
1562
  }
1448
1563
  throw new Error("Not inside a Flare app (no package.json depending on vinext found). Run this from your app directory.");
1449
1564
  }
1450
- function resolveBin(appRoot, pkgName, binName) {
1451
- const pkgDir = findUp(join6("node_modules", pkgName, "package.json"), appRoot);
1565
+ function resolveBin(appRoot, pkgName, binName2) {
1566
+ const pkgDir = findUp(join7("node_modules", pkgName, "package.json"), appRoot);
1452
1567
  if (!pkgDir) {
1453
1568
  throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
1454
1569
  }
1455
- const manifestPath = join6(pkgDir, "node_modules", pkgName, "package.json");
1570
+ const manifestPath = join7(pkgDir, "node_modules", pkgName, "package.json");
1456
1571
  const { bin } = JSON.parse(readFileSync5(manifestPath, "utf8"));
1457
- const relativeBin = typeof bin === "string" ? bin : bin?.[binName];
1458
- if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName}" executable.`);
1459
- return join6(dirname4(manifestPath), relativeBin);
1572
+ const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
1573
+ if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1574
+ return join7(dirname4(manifestPath), relativeBin);
1460
1575
  }
1461
1576
  function delegatedArgv(appRoot, command, forwarded) {
1462
1577
  const spec = DELEGATED_COMMANDS[command];
@@ -1464,15 +1579,60 @@ function delegatedArgv(appRoot, command, forwarded) {
1464
1579
  }
1465
1580
  function runNode(argv, cwd) {
1466
1581
  return new Promise((resolvePromise, reject) => {
1467
- const child = spawn2(process.execPath, argv, { cwd, stdio: "inherit" });
1582
+ const child = spawn3(process.execPath, argv, { cwd, stdio: "inherit" });
1468
1583
  child.on("error", reject);
1469
1584
  child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
1470
1585
  });
1471
1586
  }
1587
+ function runNodeWithTunnel(argv, cwd, opened) {
1588
+ return new Promise((resolvePromise, reject) => {
1589
+ const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
1590
+ let tunnel = opened;
1591
+ let seen = "";
1592
+ let announced = false;
1593
+ const watch = (chunk, out) => {
1594
+ out.write(chunk);
1595
+ if (announced) return;
1596
+ seen += chunk.toString();
1597
+ const local = parseLocalUrl(seen);
1598
+ if (!local) return;
1599
+ announced = true;
1600
+ if (opened) {
1601
+ process.stdout.write(tunnelBanner(opened.url, local));
1602
+ return;
1603
+ }
1604
+ openTunnel(local).then(
1605
+ (fresh) => {
1606
+ tunnel = fresh;
1607
+ if (child.exitCode !== null) fresh.close();
1608
+ else process.stdout.write(tunnelBanner(fresh.url, local));
1609
+ },
1610
+ (error) => process.stderr.write(pc4.red(`
1611
+ The tunnel couldn't start: ${error.message}
1612
+ The server is still running locally.
1613
+ `))
1614
+ );
1615
+ };
1616
+ child.stdout.on("data", (chunk) => watch(chunk, process.stdout));
1617
+ child.stderr.on("data", (chunk) => watch(chunk, process.stderr));
1618
+ child.on("error", reject);
1619
+ child.on("exit", (code, signal) => {
1620
+ tunnel?.close();
1621
+ resolvePromise(code ?? (signal ? 1 : 0));
1622
+ });
1623
+ });
1624
+ }
1625
+ function portArg(args) {
1626
+ const index = args.findIndex((arg) => arg === "--port" || arg === "-p");
1627
+ if (index !== -1) return args[index + 1];
1628
+ return args.find((arg) => arg.startsWith("--port="))?.slice("--port=".length);
1629
+ }
1472
1630
  async function runDelegated(command, forwarded, cwd = process.cwd()) {
1473
1631
  const appRoot = findAppRoot(cwd);
1632
+ const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1633
+ if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1474
1634
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1475
- if (command === "start" && !wantsHelp && !existsSync7(join6(appRoot, "dist/server/wrangler.json"))) {
1635
+ if (command === "start" && !wantsHelp && !existsSync8(join7(appRoot, "dist/server/wrangler.json"))) {
1476
1636
  console.log("No production build found; running `flare build` first.\n");
1477
1637
  const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1478
1638
  if (code !== 0) return code;
@@ -1484,6 +1644,12 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
1484
1644
  deploy: (args) => runNode(delegatedArgv(appRoot, "deploy", args), appRoot)
1485
1645
  });
1486
1646
  }
1647
+ if (tunnel && !wantsHelp && command === "start") {
1648
+ const opened = await openTunnel(`http://127.0.0.1:${portArg(forwarded) ?? "8787"}`);
1649
+ const origin = ["--local-upstream", new URL(opened.url).host, "--upstream-protocol", "https"];
1650
+ return runNodeWithTunnel(delegatedArgv(appRoot, command, [...forwarded, ...origin]), appRoot, opened);
1651
+ }
1652
+ if (tunnel && !wantsHelp) return runNodeWithTunnel(delegatedArgv(appRoot, command, forwarded), appRoot);
1487
1653
  return runNode(delegatedArgv(appRoot, command, forwarded), appRoot);
1488
1654
  }
1489
1655
 
@@ -1516,14 +1682,14 @@ function snapshotColumns(snapshot) {
1516
1682
  }
1517
1683
 
1518
1684
  // src/generator/migrations.ts
1519
- var listSql = (dir) => existsSync8(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1685
+ var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1520
1686
  async function generateSchemaMigration(appRoot, name, options2 = {}) {
1521
- const dir = join7(appRoot, "migrations");
1687
+ const dir = join8(appRoot, "migrations");
1522
1688
  const before = new Set(listSql(dir));
1523
1689
  const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
1524
1690
  const { code, output } = await new Promise((resolve6, reject) => {
1525
1691
  const args = ["generate", "--name", name, ...options2.custom ? ["--custom"] : []];
1526
- const child = spawn3(process.execPath, [bin, ...args], {
1692
+ const child = spawn4(process.execPath, [bin, ...args], {
1527
1693
  cwd: appRoot,
1528
1694
  stdio: ["ignore", "pipe", "pipe"]
1529
1695
  });
@@ -1546,10 +1712,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1546
1712
  for (const file of created) {
1547
1713
  const previous = previousSnapshot(dir, file);
1548
1714
  if (!previous) continue;
1549
- const path = join7(dir, file);
1715
+ const path = join8(dir, file);
1550
1716
  const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
1551
1717
  if (changes.length) {
1552
- writeFileSync4(path, sql2);
1718
+ writeFileSync5(path, sql2);
1553
1719
  repairs.push(...changes.map((change) => `${file}: ${change}`));
1554
1720
  }
1555
1721
  }
@@ -1557,32 +1723,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1557
1723
  }
1558
1724
  function previousSnapshot(dir, migrationFile) {
1559
1725
  const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
1560
- const metaDir = join7(dir, "meta");
1561
- if (!prefix || !existsSync8(metaDir)) return void 0;
1726
+ const metaDir = join8(dir, "meta");
1727
+ if (!prefix || !existsSync9(metaDir)) return void 0;
1562
1728
  const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
1563
- return earlier ? JSON.parse(readFileSync6(join7(metaDir, earlier), "utf8")) : void 0;
1729
+ return earlier ? JSON.parse(readFileSync6(join8(metaDir, earlier), "utf8")) : void 0;
1564
1730
  }
1565
1731
 
1566
1732
  // src/generator/plan.ts
1567
- import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1568
- import { dirname as dirname5, join as join9 } from "path";
1569
- import pc3 from "picocolors";
1733
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1734
+ import { dirname as dirname5, join as join10 } from "path";
1735
+ import pc5 from "picocolors";
1570
1736
 
1571
1737
  // src/generator/policy.ts
1572
- import { existsSync as existsSync9, readdirSync as readdirSync5 } from "fs";
1573
- import { join as join8, resolve as resolve5 } from "path";
1738
+ import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1739
+ import { join as join9, resolve as resolve5 } from "path";
1574
1740
  import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
1575
1741
  import { createJiti as createJiti3 } from "jiti";
1576
1742
  var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
1577
1743
  var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
1578
1744
  async function loadPolicies(root) {
1579
1745
  const appRoot = resolve5(root);
1580
- const dir = join8(appRoot, "policies");
1581
- if (!existsSync9(dir)) return [];
1582
- const jiti = createJiti3(join8(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1746
+ const dir = join9(appRoot, "policies");
1747
+ if (!existsSync10(dir)) return [];
1748
+ const jiti = createJiti3(join9(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1583
1749
  const loaded = [];
1584
1750
  for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
1585
- const mod = await jiti.import(join8(dir, file));
1751
+ const mod = await jiti.import(join9(dir, file));
1586
1752
  const policy = mod.default;
1587
1753
  if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
1588
1754
  throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
@@ -1923,7 +2089,7 @@ function planFiles(all, policies) {
1923
2089
  }
1924
2090
  function applyPlan(appRoot, plan, options2 = {}) {
1925
2091
  return plan.map(({ path, content, header }) => {
1926
- const absolute = join9(appRoot, path);
2092
+ const absolute = join10(appRoot, path);
1927
2093
  const status = inspectGenerated(absolute, content);
1928
2094
  if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
1929
2095
  if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
@@ -1942,37 +2108,37 @@ function applyPlan(appRoot, plan, options2 = {}) {
1942
2108
  function findOrphans(appRoot, all) {
1943
2109
  const planned = new Set(planFiles(all).map((file) => file.path));
1944
2110
  const candidates = [];
1945
- const list2 = (dir, match) => existsSync10(join9(appRoot, dir)) ? readdirSync6(join9(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2111
+ const list2 = (dir, match) => existsSync11(join10(appRoot, dir)) ? readdirSync6(join10(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
1946
2112
  candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
1947
2113
  candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
1948
2114
  for (const dir of list2("app/api", () => true)) {
1949
- for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync10(join9(appRoot, file))) candidates.push(file);
2115
+ for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
1950
2116
  }
1951
2117
  for (const dir of list2("app/admin", () => true)) {
1952
2118
  for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
1953
- if (existsSync10(join9(appRoot, file))) candidates.push(file);
2119
+ if (existsSync11(join10(appRoot, file))) candidates.push(file);
1954
2120
  }
1955
2121
  }
1956
2122
  const orphans = [];
1957
2123
  for (const path of candidates) {
1958
- const match = HEADER_PATTERN.exec(readFileSync7(join9(appRoot, path), "utf8"));
2124
+ const match = HEADER_PATTERN.exec(readFileSync7(join10(appRoot, path), "utf8"));
1959
2125
  if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
1960
2126
  }
1961
2127
  return orphans;
1962
2128
  }
1963
2129
  function ensureSupportFiles(appRoot, log) {
1964
2130
  for (const path of ["lib/api.ts", "lib/cache.ts"]) {
1965
- const target = join9(appRoot, path);
1966
- if (existsSync10(target)) continue;
1967
- mkdirSync3(dirname5(target), { recursive: true });
1968
- copyFileSync(join9(templatesDir, "app", path), target);
1969
- log(`${pc3.green("create".padEnd(9))} ${path}`);
2131
+ const target = join10(appRoot, path);
2132
+ if (existsSync11(target)) continue;
2133
+ mkdirSync4(dirname5(target), { recursive: true });
2134
+ copyFileSync(join10(templatesDir, "app", path), target);
2135
+ log(`${pc5.green("create".padEnd(9))} ${path}`);
1970
2136
  }
1971
2137
  }
1972
2138
  function logResults(results, log, options2 = {}) {
1973
2139
  for (const { path, outcome } of results) {
1974
2140
  if (outcome === "identical" && !options2.showIdentical) continue;
1975
- const label = outcome === "create" ? pc3.green("create") : outcome === "update" ? pc3.yellow("update") : outcome === "identical" ? pc3.dim("identical") : pc3.red(outcome === "skipped-drift" ? "drift" : "hand-written");
2141
+ const label = outcome === "create" ? pc5.green("create") : outcome === "update" ? pc5.yellow("update") : outcome === "identical" ? pc5.dim("identical") : pc5.red(outcome === "skipped-drift" ? "drift" : "hand-written");
1976
2142
  log(`${label.padEnd(18)} ${path}`);
1977
2143
  }
1978
2144
  }
@@ -1993,14 +2159,14 @@ function billingFiles(appName) {
1993
2159
  }
1994
2160
  var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
1995
2161
  function appendVars(appRoot, target) {
1996
- const path = join10(appRoot, target);
1997
- const text = existsSync11(path) ? readFileSync8(path, "utf8") : "";
2162
+ const path = join11(appRoot, target);
2163
+ const text = existsSync12(path) ? readFileSync8(path, "utf8") : "";
1998
2164
  if (/STRIPE_SECRET_KEY/.test(text)) return false;
1999
2165
  appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
2000
2166
  return true;
2001
2167
  }
2002
2168
  function addStripeDependency(appRoot) {
2003
- const path = join10(appRoot, "package.json");
2169
+ const path = join11(appRoot, "package.json");
2004
2170
  const pkg = JSON.parse(readFileSync8(path, "utf8"));
2005
2171
  pkg.dependencies ??= {};
2006
2172
  if (pkg.dependencies.stripe) return false;
@@ -2010,18 +2176,18 @@ function addStripeDependency(appRoot) {
2010
2176
  }
2011
2177
  function writeBillingResource(appRoot, name, force, log) {
2012
2178
  const relative2 = descriptorPath(name);
2013
- const path = join10(appRoot, relative2);
2014
- if (!existsSync11(path)) {
2015
- mkdirSync4(dirname6(path), { recursive: true });
2016
- writeFileSync5(path, renderBillingDescriptor(name));
2017
- log(`${pc4.green("create".padEnd(9))} ${relative2}`);
2179
+ const path = join11(appRoot, relative2);
2180
+ if (!existsSync12(path)) {
2181
+ mkdirSync5(dirname6(path), { recursive: true });
2182
+ writeFileSync6(path, renderBillingDescriptor(name));
2183
+ log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2018
2184
  return;
2019
2185
  }
2020
2186
  const parts = splitMarkers(readFileSync8(path, "utf8"));
2021
2187
  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]}`);
2022
2188
  const merged = mergeBillingFields(name, parts.block);
2023
2189
  if (!merged) {
2024
- log(`${pc4.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2190
+ log(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2025
2191
  return;
2026
2192
  }
2027
2193
  try {
@@ -2032,7 +2198,7 @@ function writeBillingResource(appRoot, name, force, log) {
2032
2198
  }
2033
2199
  throw error;
2034
2200
  }
2035
- log(`${pc4.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2201
+ log(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2036
2202
  }
2037
2203
  async function genBilling(options2 = {}) {
2038
2204
  const log = options2.log ?? ((message) => console.log(message));
@@ -2041,38 +2207,38 @@ async function genBilling(options2 = {}) {
2041
2207
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log);
2042
2208
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2043
2209
  const relative2 = policyPath(name);
2044
- if (existsSync11(join10(appRoot, relative2))) {
2045
- log(`${pc4.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2210
+ if (existsSync12(join11(appRoot, relative2))) {
2211
+ log(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2046
2212
  continue;
2047
2213
  }
2048
- mkdirSync4(join10(appRoot, "policies"), { recursive: true });
2049
- writeFileSync5(join10(appRoot, relative2), renderPolicy(name, roles));
2050
- log(`${pc4.green("create".padEnd(9))} ${relative2}`);
2214
+ mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2215
+ writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2216
+ log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2051
2217
  }
2052
2218
  const files = [];
2053
- const appName = JSON.parse(readFileSync8(join10(appRoot, "package.json"), "utf8")).name ?? "app";
2219
+ const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2054
2220
  for (const { path, content } of billingFiles(appName)) {
2055
- const outcome = writeGenerated(join10(appRoot, path), content, { header: billingHeader(), force: options2.force });
2221
+ const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2056
2222
  files.push({ path, status: "missing", outcome });
2057
- const label = outcome === "create" ? pc4.green("create") : outcome === "update" ? pc4.yellow("update") : pc4.dim("identical");
2223
+ const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2058
2224
  log(`${label.padEnd(18)} ${path}`);
2059
2225
  }
2060
2226
  for (const path of RETIRED_FILES) {
2061
- const absolute = join10(appRoot, path);
2062
- if (!existsSync11(absolute)) continue;
2227
+ const absolute = join11(appRoot, path);
2228
+ if (!existsSync12(absolute)) continue;
2063
2229
  const parts = splitMarkers(readFileSync8(absolute, "utf8"));
2064
2230
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2065
2231
  if (untouched) {
2066
2232
  unlinkSync(absolute);
2067
- log(`${pc4.red("remove".padEnd(9))} ${path} (no longer generated)`);
2233
+ log(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2068
2234
  } else {
2069
- log(pc4.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2235
+ log(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2070
2236
  }
2071
2237
  }
2072
- if (appendVars(appRoot, ".dev.vars.example")) log(`${pc4.green("append".padEnd(9))} .dev.vars.example`);
2073
- if (appendVars(appRoot, ".dev.vars")) log(`${pc4.green("append".padEnd(9))} .dev.vars`);
2238
+ if (appendVars(appRoot, ".dev.vars.example")) log(`${pc6.green("append".padEnd(9))} .dev.vars.example`);
2239
+ if (appendVars(appRoot, ".dev.vars")) log(`${pc6.green("append".padEnd(9))} .dev.vars`);
2074
2240
  const addedStripe = addStripeDependency(appRoot);
2075
- if (addedStripe) log(`${pc4.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2241
+ if (addedStripe) log(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2076
2242
  if (!options2.skipInstall) {
2077
2243
  const pm = detectPackageManager();
2078
2244
  const code = run(pm, ["install"], appRoot);
@@ -2086,27 +2252,27 @@ async function genBilling(options2 = {}) {
2086
2252
  if (!options2.skipMigration) {
2087
2253
  const result = await generateSchemaMigration(appRoot, "billing");
2088
2254
  migrations = result.files;
2089
- for (const file of migrations) log(`${pc4.green("create".padEnd(9))} ${file}`);
2090
- for (const repair of result.repairs) log(pc4.dim(`repaired ${repair}`));
2255
+ for (const file of migrations) log(`${pc6.green("create".padEnd(9))} ${file}`);
2256
+ for (const repair of result.repairs) log(pc6.dim(`repaired ${repair}`));
2091
2257
  }
2092
2258
  log(
2093
2259
  [
2094
2260
  "",
2095
2261
  `Next:`,
2096
2262
  ` 1. Put a Stripe test key in .dev.vars (STRIPE_SECRET_KEY, ideally a restricted rk_test_ key).`,
2097
- ` 2. ${pc4.bold("flare migrate")} to create the billing tables.`,
2098
- ` 3. Create one Product per plan in Stripe with metadata flare_app=${appName}, then ${pc4.bold("flare billing:sync-plans")}.`,
2099
- ` 4. ${pc4.bold("stripe listen --forward-to localhost:8787/api/webhooks/stripe")} and put its signing secret in STRIPE_WEBHOOK_SECRET.`,
2100
- ` 5. ${pc4.bold("flare start")} and open ${pc4.bold("/dashboard/billing")}.`
2263
+ ` 2. ${pc6.bold("flare migrate")} to create the billing tables.`,
2264
+ ` 3. Create one Product per plan in Stripe with metadata flare_app=${appName}, then ${pc6.bold("flare billing:sync-plans")}.`,
2265
+ ` 4. ${pc6.bold("stripe listen --forward-to localhost:8787/api/webhooks/stripe")} and put its signing secret in STRIPE_WEBHOOK_SECRET.`,
2266
+ ` 5. ${pc6.bold("flare start")} and open ${pc6.bold("/dashboard/billing")}.`
2101
2267
  ].join("\n")
2102
2268
  );
2103
2269
  return { files, migrations, addedStripe };
2104
2270
  }
2105
2271
 
2106
2272
  // src/commands/gen-security.ts
2107
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
2108
- import { dirname as dirname7, join as join11 } from "path";
2109
- import pc5 from "picocolors";
2273
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2274
+ import { dirname as dirname7, join as join12 } from "path";
2275
+ import pc7 from "picocolors";
2110
2276
 
2111
2277
  // src/generator/security.ts
2112
2278
  import { applyEdits, modify, parse as parseJsonc2 } from "jsonc-parser";
@@ -2633,49 +2799,49 @@ function securityFiles(appName) {
2633
2799
  }
2634
2800
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2635
2801
  function writeOnce(appRoot, relative2, content, log) {
2636
- const path = join11(appRoot, relative2);
2637
- if (existsSync12(path)) {
2638
- log(`${pc5.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2802
+ const path = join12(appRoot, relative2);
2803
+ if (existsSync13(path)) {
2804
+ log(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2639
2805
  return;
2640
2806
  }
2641
- mkdirSync5(dirname7(path), { recursive: true });
2642
- writeFileSync6(path, content);
2643
- log(`${pc5.green("create".padEnd(9))} ${relative2}`);
2807
+ mkdirSync6(dirname7(path), { recursive: true });
2808
+ writeFileSync7(path, content);
2809
+ log(`${pc7.green("create".padEnd(9))} ${relative2}`);
2644
2810
  }
2645
2811
  async function genSecurity(options2 = {}) {
2646
2812
  const log = options2.log ?? ((message) => console.log(message));
2647
2813
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2648
- const appName = JSON.parse(readFileSync9(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2814
+ const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2649
2815
  writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log);
2650
2816
  writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log);
2651
2817
  writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log);
2652
2818
  const files = [];
2653
2819
  const write = (path, content, header) => {
2654
- const absolute = join11(appRoot, path);
2655
- if (path === "lib/security.ts" && existsSync12(absolute)) {
2820
+ const absolute = join12(appRoot, path);
2821
+ if (path === "lib/security.ts" && existsSync13(absolute)) {
2656
2822
  const source = readFileSync9(absolute, "utf8");
2657
- if (PASS_THROUGH_HEADER.test(source)) writeFileSync6(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
2823
+ if (PASS_THROUGH_HEADER.test(source)) writeFileSync7(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
2658
2824
  }
2659
2825
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
2660
2826
  files.push({ path, status: "missing", outcome });
2661
- const label = outcome === "create" ? pc5.green("create") : outcome === "update" ? pc5.yellow("update") : pc5.dim("identical");
2827
+ const label = outcome === "create" ? pc7.green("create") : outcome === "update" ? pc7.yellow("update") : pc7.dim("identical");
2662
2828
  log(`${label.padEnd(18)} ${path}`);
2663
2829
  };
2664
2830
  for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
2665
- const navPath = join11(appRoot, "lib/admin-nav.ts");
2666
- if (existsSync12(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/admin-nav.ts", renderAdminNavBlock());
2667
- else log(pc5.yellow(`lib/admin-nav.ts not found: link to /admin/security from your admin sidebar yourself.`));
2668
- const wranglerPath = join11(appRoot, "wrangler.jsonc");
2831
+ const navPath = join12(appRoot, "lib/admin-nav.ts");
2832
+ if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/admin-nav.ts", renderAdminNavBlock());
2833
+ else log(pc7.yellow(`lib/admin-nav.ts not found: link to /admin/security from your admin sidebar yourself.`));
2834
+ const wranglerPath = join12(appRoot, "wrangler.jsonc");
2669
2835
  let bindings = [];
2670
- if (existsSync12(wranglerPath)) {
2836
+ if (existsSync13(wranglerPath)) {
2671
2837
  const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
2672
2838
  bindings = added;
2673
2839
  if (added.length) {
2674
- writeFileSync6(wranglerPath, text);
2675
- for (const line of added) log(`${pc5.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2840
+ writeFileSync7(wranglerPath, text);
2841
+ for (const line of added) log(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2676
2842
  }
2677
2843
  } else {
2678
- log(pc5.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2844
+ log(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2679
2845
  }
2680
2846
  const all = await loadResources(appRoot);
2681
2847
  ensureSupportFiles(appRoot, log);
@@ -2684,17 +2850,17 @@ async function genSecurity(options2 = {}) {
2684
2850
  if (!options2.skipMigration) {
2685
2851
  const result = await generateSchemaMigration(appRoot, "security");
2686
2852
  migrations = result.files;
2687
- for (const file of migrations) log(`${pc5.green("create".padEnd(9))} ${file}`);
2688
- for (const repair of result.repairs) log(pc5.dim(`repaired ${repair}`));
2853
+ for (const file of migrations) log(`${pc7.green("create".padEnd(9))} ${file}`);
2854
+ for (const repair of result.repairs) log(pc7.dim(`repaired ${repair}`));
2689
2855
  }
2690
2856
  log(
2691
2857
  [
2692
2858
  "",
2693
2859
  `Next:`,
2694
- ` 1. ${pc5.bold("flare migrate")} to create the security_events table.`,
2695
- ` 2. ${pc5.bold("flare dev")}, then open ${pc5.bold("/admin/security")}. Tune detectors in security.config.ts.`,
2860
+ ` 1. ${pc7.bold("flare migrate")} to create the security_events table.`,
2861
+ ` 2. ${pc7.bold("flare dev")}, then open ${pc7.bold("/admin/security")}. Tune detectors in security.config.ts.`,
2696
2862
  ` 3. Optional zone layer: deploy with FLARE_SECURITY_ZONE_ID and FLARE_SECURITY_API_TOKEN set (a token scoped to that`,
2697
- ` one zone with Zone WAF Edit, Firewall Services Edit, Analytics Read, Zone Read). ${pc5.bold("flare deploy")} pushes the`,
2863
+ ` one zone with Zone WAF Edit, Firewall Services Edit, Analytics Read, Zone Read). ${pc7.bold("flare deploy")} pushes the`,
2698
2864
  ` zone rules and uploads both as secrets.`
2699
2865
  ].join("\n")
2700
2866
  );
@@ -2702,10 +2868,10 @@ async function genSecurity(options2 = {}) {
2702
2868
  }
2703
2869
 
2704
2870
  // src/commands/gen.ts
2705
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
2706
- import { dirname as dirname8, join as join12 } from "path";
2871
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2872
+ import { dirname as dirname8, join as join13 } from "path";
2707
2873
  import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
2708
- import pc6 from "picocolors";
2874
+ import pc8 from "picocolors";
2709
2875
  function validateFields(name, fields, others, log) {
2710
2876
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
2711
2877
  for (const field of fields) {
@@ -2716,7 +2882,7 @@ function validateFields(name, fields, others, log) {
2716
2882
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
2717
2883
  const graph = relationGraph2([...others, candidate]);
2718
2884
  for (const pending of graph.pending.filter((p) => p.resource === name)) {
2719
- log(pc6.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
2885
+ log(pc8.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
2720
2886
  }
2721
2887
  }
2722
2888
  async function genResource(rawName, options2) {
@@ -2724,8 +2890,8 @@ async function genResource(rawName, options2) {
2724
2890
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2725
2891
  const name = resourceName(rawName);
2726
2892
  const relativePath = descriptorPath(name);
2727
- const path = join12(appRoot, relativePath);
2728
- const exists = existsSync13(path);
2893
+ const path = join13(appRoot, relativePath);
2894
+ const exists = existsSync14(path);
2729
2895
  if (!exists && !options2.fields) {
2730
2896
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
2731
2897
  }
@@ -2735,13 +2901,13 @@ async function genResource(rawName, options2) {
2735
2901
  const fields = parseFields(options2.fields);
2736
2902
  validateFields(name, fields, others, log);
2737
2903
  if (!exists) {
2738
- mkdirSync6(dirname8(path), { recursive: true });
2739
- writeFileSync7(path, renderDescriptor(name, fields));
2740
- log(`${pc6.green("create".padEnd(9))} ${relativePath}`);
2904
+ mkdirSync7(dirname8(path), { recursive: true });
2905
+ writeFileSync8(path, renderDescriptor(name, fields));
2906
+ log(`${pc8.green("create".padEnd(9))} ${relativePath}`);
2741
2907
  } else {
2742
2908
  try {
2743
2909
  const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
2744
- log(`${(outcome === "identical" ? pc6.dim : pc6.yellow)(outcome.padEnd(9))} ${relativePath}`);
2910
+ log(`${(outcome === "identical" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relativePath}`);
2745
2911
  } catch (error) {
2746
2912
  if (error instanceof DriftError) {
2747
2913
  throw new Error(
@@ -2767,28 +2933,28 @@ async function genResource(rawName, options2) {
2767
2933
  }
2768
2934
  const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
2769
2935
  if (skipped.length) {
2770
- log(pc6.yellow(`
2936
+ log(pc8.yellow(`
2771
2937
  Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
2772
2938
  }
2773
2939
  let migrations = [];
2774
2940
  if (!options2.skipMigration) {
2775
2941
  const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
2776
2942
  migrations = result.files;
2777
- for (const file of migrations) log(`${pc6.green("create".padEnd(9))} ${file}`);
2778
- for (const repair of result.repairs) log(pc6.dim(`repaired ${repair}`));
2943
+ for (const file of migrations) log(`${pc8.green("create".padEnd(9))} ${file}`);
2944
+ for (const repair of result.repairs) log(pc8.dim(`repaired ${repair}`));
2779
2945
  if (migrations.length) log(`
2780
- Next: ${pc6.bold("flare migrate")} to apply it locally.`);
2781
- else if (exists) log(pc6.dim("\nNo table changes, so no migration."));
2946
+ Next: ${pc8.bold("flare migrate")} to apply it locally.`);
2947
+ else if (exists) log(pc8.dim("\nNo table changes, so no migration."));
2782
2948
  }
2783
2949
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
2784
2950
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
2785
2951
  }
2786
2952
 
2787
2953
  // src/commands/gen-migration.ts
2788
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2789
- import { basename as basename2, join as join13 } from "path";
2954
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
2955
+ import { basename as basename2, join as join14 } from "path";
2790
2956
  import { snakeCase } from "@flaredev/core";
2791
- import pc7 from "picocolors";
2957
+ import pc9 from "picocolors";
2792
2958
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
2793
2959
  -- Write SQL that undoes the up migration, e.g.:
2794
2960
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
@@ -2804,31 +2970,31 @@ async function genMigration(rawName, options2 = {}) {
2804
2970
  log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
2805
2971
  return [];
2806
2972
  }
2807
- for (const file of files) log(`${pc7.green("create".padEnd(9))} ${file}`);
2808
- for (const repair of repairs) log(pc7.dim(`repaired ${repair}`));
2973
+ for (const file of files) log(`${pc9.green("create".padEnd(9))} ${file}`);
2974
+ for (const repair of repairs) log(pc9.dim(`repaired ${repair}`));
2809
2975
  if (!options2.fromSchema) {
2810
- const downDir = join13(appRoot, "migrations", "down");
2811
- mkdirSync7(downDir, { recursive: true });
2976
+ const downDir = join14(appRoot, "migrations", "down");
2977
+ mkdirSync8(downDir, { recursive: true });
2812
2978
  for (const file of files) {
2813
- const down = join13(downDir, basename2(file));
2814
- if (existsSync14(down)) continue;
2815
- writeFileSync8(down, DOWN_TEMPLATE(basename2(file)));
2816
- log(`${pc7.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
2979
+ const down = join14(downDir, basename2(file));
2980
+ if (existsSync15(down)) continue;
2981
+ writeFileSync9(down, DOWN_TEMPLATE(basename2(file)));
2982
+ log(`${pc9.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
2817
2983
  }
2818
2984
  log(`
2819
- Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc7.bold("flare migrate")}.`);
2985
+ Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc9.bold("flare migrate")}.`);
2820
2986
  } else {
2821
2987
  log(`
2822
- Next: ${pc7.bold("flare migrate")}.`);
2988
+ Next: ${pc9.bold("flare migrate")}.`);
2823
2989
  }
2824
2990
  return files;
2825
2991
  }
2826
2992
 
2827
2993
  // src/commands/gen-policy.ts
2828
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
2829
- import { dirname as dirname9, join as join14 } from "path";
2994
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
2995
+ import { dirname as dirname9, join as join15 } from "path";
2830
2996
  import { definePolicy } from "@flaredev/core";
2831
- import pc8 from "picocolors";
2997
+ import pc10 from "picocolors";
2832
2998
  function parseRoles(input, what = "--roles") {
2833
2999
  const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
2834
3000
  if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
@@ -2844,8 +3010,8 @@ async function genPolicy(rawName, options2) {
2844
3010
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
2845
3011
  }
2846
3012
  const relative2 = policyPath(name);
2847
- const path = join14(appRoot, relative2);
2848
- const exists = existsSync15(path);
3013
+ const path = join15(appRoot, relative2);
3014
+ const exists = existsSync16(path);
2849
3015
  let roles;
2850
3016
  let deleteRoles;
2851
3017
  if (options2.roles || !exists) {
@@ -2856,13 +3022,13 @@ async function genPolicy(rawName, options2) {
2856
3022
  if (roles && deleteRoles) {
2857
3023
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
2858
3024
  if (!exists) {
2859
- mkdirSync8(dirname9(path), { recursive: true });
2860
- writeFileSync9(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
2861
- log(`${pc8.green("create".padEnd(9))} ${relative2}`);
3025
+ mkdirSync9(dirname9(path), { recursive: true });
3026
+ writeFileSync10(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3027
+ log(`${pc10.green("create".padEnd(9))} ${relative2}`);
2862
3028
  } else {
2863
3029
  try {
2864
3030
  const outcome = writeGenerated(path, block, { force: options2.force });
2865
- log(`${(outcome === "identical" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relative2}`);
3031
+ log(`${(outcome === "identical" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relative2}`);
2866
3032
  } catch (error) {
2867
3033
  if (error instanceof DriftError) {
2868
3034
  throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
@@ -2874,15 +3040,15 @@ async function genPolicy(rawName, options2) {
2874
3040
  const policies = await loadPolicies(appRoot);
2875
3041
  logResults(applyPlan(appRoot, planFiles(resources, policies)), log);
2876
3042
  log(`
2877
- ${pc8.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3043
+ ${pc10.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
2878
3044
  return { name, path: relative2 };
2879
3045
  }
2880
3046
 
2881
3047
  // src/commands/migrate.ts
2882
- import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync10, rmSync, writeFileSync as writeFileSync10 } from "fs";
3048
+ import { existsSync as existsSync17, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync11 } from "fs";
2883
3049
  import { tmpdir } from "os";
2884
- import { join as join15 } from "path";
2885
- import pc9 from "picocolors";
3050
+ import { join as join16 } from "path";
3051
+ import pc11 from "picocolors";
2886
3052
 
2887
3053
  // src/generator/down.ts
2888
3054
  var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
@@ -2999,7 +3165,7 @@ async function rollback(options2 = {}) {
2999
3165
  const steps = options2.steps ?? 1;
3000
3166
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3001
3167
  const { appRoot, db, target, where, wrangler, log } = context(options2);
3002
- const migrationsDir = join15(appRoot, db.migrations_dir ?? "migrations");
3168
+ const migrationsDir = join16(appRoot, db.migrations_dir ?? "migrations");
3003
3169
  const applied = await appliedMigrations(options2, steps);
3004
3170
  if (applied.length === 0) {
3005
3171
  log("Nothing to roll back.");
@@ -3008,8 +3174,8 @@ async function rollback(options2 = {}) {
3008
3174
  const plan = [];
3009
3175
  for (const { name } of applied) {
3010
3176
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
3011
- const handWritten = join15(migrationsDir, "down", name);
3012
- if (existsSync16(handWritten)) {
3177
+ const handWritten = join16(migrationsDir, "down", name);
3178
+ if (existsSync17(handWritten)) {
3013
3179
  const sql2 = readFileSync10(handWritten, "utf8");
3014
3180
  if (splitStatements(sql2).length === 0) {
3015
3181
  throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
@@ -3017,8 +3183,8 @@ async function rollback(options2 = {}) {
3017
3183
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3018
3184
  continue;
3019
3185
  }
3020
- const upPath = join15(migrationsDir, name);
3021
- if (!existsSync16(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3186
+ const upPath = join16(migrationsDir, name);
3187
+ if (!existsSync17(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3022
3188
  const down = deriveDown(readFileSync10(upPath, "utf8"));
3023
3189
  if (!down.reversible) {
3024
3190
  throw new Error(
@@ -3028,37 +3194,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3028
3194
  }
3029
3195
  plan.push({ name, sql: down.sql, source: "derived" });
3030
3196
  }
3031
- log(`${pc9.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3197
+ log(`${pc11.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3032
3198
  for (const step of plan) {
3033
- log(` ${step.name} ${pc9.dim(`(${step.source})`)}`);
3034
- for (const line of step.sql.trim().split("\n")) log(pc9.dim(` ${line}`));
3199
+ log(` ${step.name} ${pc11.dim(`(${step.source})`)}`);
3200
+ for (const line of step.sql.trim().split("\n")) log(pc11.dim(` ${line}`));
3035
3201
  }
3036
3202
  if (options2.remote && !options2.yes) {
3037
- log(pc9.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3203
+ log(pc11.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3038
3204
  return 1;
3039
3205
  }
3040
3206
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3041
3207
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3042
- const dir = mkdtempSync(join15(tmpdir(), "flare-rollback-"));
3208
+ const dir = mkdtempSync(join16(tmpdir(), "flare-rollback-"));
3043
3209
  try {
3044
- const file = join15(dir, "rollback.sql");
3045
- writeFileSync10(file, script);
3210
+ const file = join16(dir, "rollback.sql");
3211
+ writeFileSync11(file, script);
3046
3212
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3047
3213
  if (result.code !== 0) throw new Error(`Rollback failed:
3048
3214
  ${result.output.trim()}`);
3049
3215
  } finally {
3050
- rmSync(dir, { recursive: true, force: true });
3216
+ rmSync2(dir, { recursive: true, force: true });
3051
3217
  }
3052
- log(pc9.green(`
3218
+ log(pc11.green(`
3053
3219
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3054
3220
  return 0;
3055
3221
  }
3056
3222
 
3057
3223
  // src/commands/rm.ts
3058
- import { existsSync as existsSync17, readdirSync as readdirSync7, readFileSync as readFileSync11, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3059
- import { dirname as dirname10, join as join16 } from "path";
3224
+ import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync11, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3225
+ import { dirname as dirname10, join as join17 } from "path";
3060
3226
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3061
- import pc10 from "picocolors";
3227
+ import pc12 from "picocolors";
3062
3228
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
3063
3229
  function handWrittenReason(source, header, options2 = {}) {
3064
3230
  const parts = splitMarkers(source);
@@ -3075,10 +3241,10 @@ function handWrittenReason(source, header, options2 = {}) {
3075
3241
  return null;
3076
3242
  }
3077
3243
  function removeEmptyDirs(appRoot, start) {
3078
- let dir = dirname10(join16(appRoot, start));
3079
- const roots = [join16(appRoot, "app", "api"), join16(appRoot, "app", "admin")];
3244
+ let dir = dirname10(join17(appRoot, start));
3245
+ const roots = [join17(appRoot, "app", "api"), join17(appRoot, "app", "admin")];
3080
3246
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3081
- while (inside() && existsSync17(dir) && readdirSync7(dir).length === 0) {
3247
+ while (inside() && existsSync18(dir) && readdirSync7(dir).length === 0) {
3082
3248
  rmdirSync(dir);
3083
3249
  dir = dirname10(dir);
3084
3250
  }
@@ -3107,7 +3273,7 @@ async function rmResource(rawName, options2 = {}) {
3107
3273
  if (entry) {
3108
3274
  owned.add(entry.file);
3109
3275
  for (const file of planFiles(all)) {
3110
- if (existsSync17(join16(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3276
+ if (existsSync18(join17(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3111
3277
  }
3112
3278
  }
3113
3279
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3116,7 +3282,7 @@ async function rmResource(rawName, options2 = {}) {
3116
3282
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3117
3283
  const findings = [];
3118
3284
  for (const path of owned) {
3119
- const source = readFileSync11(join16(appRoot, path), "utf8");
3285
+ const source = readFileSync11(join17(appRoot, path), "utf8");
3120
3286
  let wrapper;
3121
3287
  if (entry && path === entry.file) {
3122
3288
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3137,34 +3303,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
3137
3303
  }
3138
3304
  const removed = [...owned].sort();
3139
3305
  for (const path of removed) {
3140
- unlinkSync2(join16(appRoot, path));
3306
+ unlinkSync2(join17(appRoot, path));
3141
3307
  removeEmptyDirs(appRoot, path);
3142
- log(`${pc10.red("remove".padEnd(9))} ${path}`);
3308
+ log(`${pc12.red("remove".padEnd(9))} ${path}`);
3143
3309
  }
3144
3310
  logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
3145
3311
  const table = entry ? tableExport(entry.resource) : void 0;
3146
- if (table && existsSync17(join16(appRoot, "seeds"))) {
3147
- const seeds = readdirSync7(join16(appRoot, "seeds")).filter(
3148
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(join16(appRoot, "seeds", file), "utf8"))
3312
+ if (table && existsSync18(join17(appRoot, "seeds"))) {
3313
+ const seeds = readdirSync7(join17(appRoot, "seeds")).filter(
3314
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(join17(appRoot, "seeds", file), "utf8"))
3149
3315
  );
3150
- if (seeds.length) log(pc10.yellow(`
3316
+ if (seeds.length) log(pc12.yellow(`
3151
3317
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
3152
3318
  }
3153
3319
  let migrations = [];
3154
3320
  if (!options2.skipMigration && entry) {
3155
3321
  const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
3156
3322
  migrations = result.files;
3157
- for (const file of migrations) log(`${pc10.green("create".padEnd(9))} ${file}`);
3158
- for (const repair of result.repairs) log(pc10.dim(`repaired ${repair}`));
3323
+ for (const file of migrations) log(`${pc12.green("create".padEnd(9))} ${file}`);
3324
+ for (const repair of result.repairs) log(pc12.dim(`repaired ${repair}`));
3159
3325
  if (migrations.length) log(`
3160
- Next: ${pc10.bold("flare migrate")} to drop the table locally.`);
3326
+ Next: ${pc12.bold("flare migrate")} to drop the table locally.`);
3161
3327
  }
3162
3328
  return { removed, migrations };
3163
3329
  }
3164
3330
 
3165
3331
  // src/commands/role.ts
3166
3332
  import { humanize } from "@flaredev/core";
3167
- import pc11 from "picocolors";
3333
+ import pc13 from "picocolors";
3168
3334
  var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
3169
3335
  function roleInsertSql(name, label) {
3170
3336
  if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
@@ -3209,24 +3375,24 @@ ${result.output.trim()}`);
3209
3375
  }
3210
3376
  const [first] = extractJson(result.output);
3211
3377
  const added = first?.results?.[0];
3212
- log(pc11.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3213
- log(pc11.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3378
+ log(pc13.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3379
+ log(pc13.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3214
3380
  return 0;
3215
3381
  }
3216
3382
 
3217
3383
  // src/commands/seed.ts
3218
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "fs";
3384
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync12 } from "fs";
3219
3385
  import { createRequire } from "module";
3220
- import { join as join17 } from "path";
3386
+ import { join as join18 } from "path";
3221
3387
  import { pathToFileURL } from "url";
3222
3388
  import { kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
3223
3389
  import { createJiti as createJiti4 } from "jiti";
3224
- import pc12 from "picocolors";
3390
+ import pc14 from "picocolors";
3225
3391
  var SEEDS_DIR = "seeds";
3226
3392
  var LOCAL_STATE = ".wrangler/state/v3";
3227
3393
  function seedFiles(appRoot, names) {
3228
- const dir = join17(appRoot, SEEDS_DIR);
3229
- const all = existsSync18(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3394
+ const dir = join18(appRoot, SEEDS_DIR);
3395
+ const all = existsSync19(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3230
3396
  if (!names?.length) return all;
3231
3397
  return names.map((name) => {
3232
3398
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3241,29 +3407,29 @@ async function runSeeds(options2 = {}) {
3241
3407
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3242
3408
  const files = seedFiles(appRoot, options2.names);
3243
3409
  if (files.length === 0) {
3244
- log(`No seeds found. Create one with ${pc12.bold("flare seed:make <name>")}.`);
3410
+ log(`No seeds found. Create one with ${pc14.bold("flare seed:make <name>")}.`);
3245
3411
  return;
3246
3412
  }
3247
- const jiti = createJiti4(join17(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3248
- const appRequire = createRequire(join17(appRoot, "package.json"));
3413
+ const jiti = createJiti4(join18(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3414
+ const appRequire = createRequire(join18(appRoot, "package.json"));
3249
3415
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3250
3416
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3251
- const schema = await jiti.import(join17(appRoot, "db/schema.ts"));
3417
+ const schema = await jiti.import(join18(appRoot, "db/schema.ts"));
3252
3418
  const { env, dispose } = await wrangler.getPlatformProxy({
3253
- configPath: join17(appRoot, "wrangler.jsonc"),
3254
- persist: { path: join17(appRoot, LOCAL_STATE) }
3419
+ configPath: join18(appRoot, "wrangler.jsonc"),
3420
+ persist: { path: join18(appRoot, LOCAL_STATE) }
3255
3421
  });
3256
3422
  try {
3257
3423
  const bindings = env;
3258
3424
  if (!bindings.DB) throw new Error("No DB binding found in wrangler.jsonc.");
3259
3425
  const db = drizzle(bindings.DB, { schema });
3260
3426
  for (const file of files) {
3261
- const mod = await jiti.import(join17(appRoot, SEEDS_DIR, file));
3427
+ const mod = await jiti.import(join18(appRoot, SEEDS_DIR, file));
3262
3428
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3263
- log(`${pc12.cyan("seed")} ${file}`);
3429
+ log(`${pc14.cyan("seed")} ${file}`);
3264
3430
  await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
3265
3431
  }
3266
- log(pc12.green(`
3432
+ log(pc14.green(`
3267
3433
  Ran ${files.length} seed(s) against the local database.`));
3268
3434
  } finally {
3269
3435
  await dispose();
@@ -3332,8 +3498,8 @@ async function makeSeed(rawName, options2 = {}) {
3332
3498
  const name = kebabCase3(rawName);
3333
3499
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3334
3500
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3335
- const path = join17(appRoot, relative2);
3336
- if (existsSync18(path)) throw new Error(`${relative2} already exists.`);
3501
+ const path = join18(appRoot, relative2);
3502
+ if (existsSync19(path)) throw new Error(`${relative2} already exists.`);
3337
3503
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3338
3504
  let resource;
3339
3505
  if (options2.resource) {
@@ -3342,26 +3508,26 @@ async function makeSeed(rawName, options2 = {}) {
3342
3508
  } else {
3343
3509
  resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
3344
3510
  }
3345
- mkdirSync9(join17(appRoot, SEEDS_DIR), { recursive: true });
3346
- writeFileSync11(path, renderSeed(resource));
3347
- log(`${pc12.green("create".padEnd(9))} ${relative2}`);
3511
+ mkdirSync10(join18(appRoot, SEEDS_DIR), { recursive: true });
3512
+ writeFileSync12(path, renderSeed(resource));
3513
+ log(`${pc14.green("create".padEnd(9))} ${relative2}`);
3348
3514
  log(`
3349
- Run it with ${pc12.bold(`flare seed ${name}`)}.`);
3515
+ Run it with ${pc14.bold(`flare seed ${name}`)}.`);
3350
3516
  return relative2;
3351
3517
  }
3352
3518
 
3353
3519
  // src/commands/billing-sync.ts
3354
3520
  import { createRequire as createRequire2 } from "module";
3355
- import { existsSync as existsSync19, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
3521
+ import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "fs";
3356
3522
  import { tmpdir as tmpdir2 } from "os";
3357
- import { join as join18 } from "path";
3523
+ import { join as join19 } from "path";
3358
3524
  import { pathToFileURL as pathToFileURL2 } from "url";
3359
3525
  import { kebabCase as kebabCase4 } from "@flaredev/core";
3360
- import pc13 from "picocolors";
3526
+ import pc15 from "picocolors";
3361
3527
  function stripeKeys(appRoot) {
3362
3528
  const map = {};
3363
- const path = join18(appRoot, ".dev.vars");
3364
- if (existsSync19(path)) {
3529
+ const path = join19(appRoot, ".dev.vars");
3530
+ if (existsSync20(path)) {
3365
3531
  for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
3366
3532
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
3367
3533
  if (match) map[match[1]] = match[2].trim();
@@ -3445,10 +3611,10 @@ async function syncPlans(options2 = {}) {
3445
3611
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
3446
3612
  const db = readD1Databases(appRoot)[0];
3447
3613
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
3448
- const appRequire = createRequire2(join18(appRoot, "package.json"));
3614
+ const appRequire = createRequire2(join19(appRoot, "package.json"));
3449
3615
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
3450
3616
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
3451
- const appName = JSON.parse(readFileSync12(join18(appRoot, "package.json"), "utf8")).name ?? "app";
3617
+ const appName = JSON.parse(readFileSync12(join19(appRoot, "package.json"), "utf8")).name ?? "app";
3452
3618
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
3453
3619
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
3454
3620
  if (products.length === 0) {
@@ -3469,41 +3635,41 @@ async function syncPlans(options2 = {}) {
3469
3635
  ${existingResult.output.trim()}`);
3470
3636
  }
3471
3637
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
3472
- const dir = mkdtempSync2(join18(tmpdir2(), "flare-sync-"));
3638
+ const dir = mkdtempSync2(join19(tmpdir2(), "flare-sync-"));
3473
3639
  try {
3474
- const file = join18(dir, "sync-plans.sql");
3475
- writeFileSync12(file, renderSyncSql(rows));
3640
+ const file = join19(dir, "sync-plans.sql");
3641
+ writeFileSync13(file, renderSyncSql(rows));
3476
3642
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3477
3643
  if (result.code !== 0) throw new Error(`Could not write plans:
3478
3644
  ${result.output.trim()}`);
3479
3645
  } finally {
3480
- rmSync2(dir, { recursive: true, force: true });
3646
+ rmSync3(dir, { recursive: true, force: true });
3481
3647
  }
3482
3648
  let created = 0;
3483
3649
  for (const row of rows) {
3484
3650
  const isNew = !known.has(row.stripePriceId);
3485
3651
  if (isNew) created++;
3486
3652
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
3487
- log(`${(isNew ? pc13.green("create") : pc13.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc13.dim(price)}`);
3653
+ log(`${(isNew ? pc15.green("create") : pc15.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc15.dim(price)}`);
3488
3654
  }
3489
- for (const name of skipped) log(pc13.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3655
+ for (const name of skipped) log(pc15.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3490
3656
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
3491
- if (configuration) log(`${pc13.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
3492
- log(pc13.green(`
3657
+ if (configuration) log(`${pc15.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
3658
+ log(pc15.green(`
3493
3659
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
3494
3660
  return { created, updated: rows.length - created, skipped };
3495
3661
  }
3496
3662
 
3497
3663
  // src/commands/sync.ts
3498
- import { existsSync as existsSync20 } from "fs";
3499
- import { join as join19 } from "path";
3500
- import pc14 from "picocolors";
3664
+ import { existsSync as existsSync21 } from "fs";
3665
+ import { join as join20 } from "path";
3666
+ import pc16 from "picocolors";
3501
3667
  async function syncTypes(options2 = {}) {
3502
3668
  const log = options2.log ?? ((message) => console.log(message));
3503
3669
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3504
3670
  const all = await loadResources(appRoot);
3505
3671
  const policies = await loadPolicies(appRoot);
3506
- const tracksPolicies = policies.length > 0 || existsSync20(join19(appRoot, "policies/index.ts"));
3672
+ const tracksPolicies = policies.length > 0 || existsSync21(join20(appRoot, "policies/index.ts"));
3507
3673
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
3508
3674
  if (!options2.check) ensureSupportFiles(appRoot, log);
3509
3675
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -3515,42 +3681,42 @@ async function syncTypes(options2 = {}) {
3515
3681
  if (drift.length) {
3516
3682
  log(
3517
3683
  `
3518
- ${pc14.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3684
+ ${pc16.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3519
3685
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
3520
3686
  );
3521
3687
  }
3522
3688
  if (unmarked.length) {
3523
3689
  log(`
3524
- ${pc14.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3690
+ ${pc16.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3525
3691
  }
3526
3692
  if (orphans.length) {
3527
3693
  const resources = [...new Set(orphans.map((o) => o.resource))];
3528
3694
  log(`
3529
- ${pc14.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
3695
+ ${pc16.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
3530
3696
  for (const orphan of orphans) log(` ${orphan.path}`);
3531
3697
  log(
3532
- `If the resource was deleted, remove them with ${pc14.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
3698
+ `If the resource was deleted, remove them with ${pc16.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
3533
3699
  );
3534
3700
  }
3535
3701
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
3536
3702
  if (tablesChanged && !options2.check) {
3537
3703
  log(`
3538
- ${pc14.cyan("Tables changed.")} Create a migration with ${pc14.bold("flare gen migration <name> --from-schema")}, then ${pc14.bold("flare migrate")}.`);
3704
+ ${pc16.cyan("Tables changed.")} Create a migration with ${pc16.bold("flare gen migration <name> --from-schema")}, then ${pc16.bold("flare migrate")}.`);
3539
3705
  }
3540
3706
  const problems = drift.length + unmarked.length + orphans.length;
3541
3707
  if (options2.check) {
3542
3708
  const stale = changed.length;
3543
- if (stale === 0 && problems === 0) log(pc14.green("Generated files are in sync with the resource descriptors."));
3709
+ if (stale === 0 && problems === 0) log(pc16.green("Generated files are in sync with the resource descriptors."));
3544
3710
  else log(`
3545
- ${pc14.red("Out of sync:")} ${stale} file(s) would change. Run ${pc14.bold("flare sync-types")}.`);
3711
+ ${pc16.red("Out of sync:")} ${stale} file(s) would change. Run ${pc16.bold("flare sync-types")}.`);
3546
3712
  return stale + problems > 0 ? 1 : 0;
3547
3713
  }
3548
- if (changed.length === 0 && problems === 0) log(pc14.green("Everything is in sync."));
3714
+ if (changed.length === 0 && problems === 0) log(pc16.green("Everything is in sync."));
3549
3715
  return problems > 0 ? 1 : 0;
3550
3716
  }
3551
3717
 
3552
3718
  // src/commands/user-role.ts
3553
- import pc15 from "picocolors";
3719
+ import pc17 from "picocolors";
3554
3720
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
3555
3721
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
3556
3722
  function userRoleSql(email, role) {
@@ -3564,8 +3730,8 @@ async function setUserRole(email, role, options2 = {}) {
3564
3730
  const sql2 = userRoleSql(email, role);
3565
3731
  const roles = await listRoles(options2);
3566
3732
  if (roles && !roles.includes(role)) {
3567
- log(pc15.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3568
- log(pc15.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
3733
+ log(pc17.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3734
+ log(pc17.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
3569
3735
  return 1;
3570
3736
  }
3571
3737
  const db = readD1Databases(appRoot)[0];
@@ -3579,10 +3745,10 @@ ${result.output.trim()}`);
3579
3745
  const [first] = extractJson(result.output);
3580
3746
  const updated = first?.results ?? [];
3581
3747
  if (updated.length === 0) {
3582
- log(pc15.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
3748
+ log(pc17.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
3583
3749
  return 1;
3584
3750
  }
3585
- log(pc15.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3751
+ log(pc17.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3586
3752
  return 0;
3587
3753
  }
3588
3754
 
@@ -3636,6 +3802,18 @@ function createCli() {
3636
3802
  cli.command("sync-types", "Regenerate schema, routes, clients, and validators from resource descriptors; report drift").option("--check", "Change nothing; exit 1 if anything is out of sync (CI)").option("--force", "Overwrite generated blocks that were edited by hand").action(async (options2) => {
3637
3803
  process.exitCode = await syncTypes(options2);
3638
3804
  });
3805
+ cli.command("tunnel [target]", "Share a local server on a public https://*.trycloudflare.com URL (Cloudflare Quick Tunnel; no account needed)").example("flare tunnel # http://localhost:3000").example("flare tunnel 8787").example("flare tunnel http://localhost:5173").action(async (target) => {
3806
+ const local = toLocalUrl(target);
3807
+ const tunnel = await openTunnel(local);
3808
+ console.log(tunnelBanner(tunnel.url, local));
3809
+ process.exitCode = await new Promise((resolve6) => {
3810
+ process.once("SIGINT", () => {
3811
+ tunnel.close();
3812
+ resolve6(0);
3813
+ });
3814
+ tunnel.process.once("exit", (code) => resolve6(code ?? 0));
3815
+ });
3816
+ });
3639
3817
  for (const [name, spec] of Object.entries(DELEGATED_COMMANDS)) {
3640
3818
  cli.command(`${name} [...args]`, spec.description).allowUnknownOptions();
3641
3819
  }
@@ -3659,6 +3837,6 @@ try {
3659
3837
  }
3660
3838
  }
3661
3839
  } catch (error) {
3662
- console.error(pc16.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
3840
+ console.error(pc18.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
3663
3841
  process.exitCode = 1;
3664
3842
  }