@flaredev/cli 0.1.0 → 0.1.2

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";
@@ -157,6 +157,15 @@ function childEnv(env = process.env) {
157
157
  }
158
158
  return next;
159
159
  }
160
+ function runQuiet(command, args, cwd) {
161
+ const env = childEnv();
162
+ const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, env, shell: true, encoding: "utf8" }) : spawnSync(command, args, { cwd, env, encoding: "utf8" });
163
+ return { code: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
164
+ }
165
+ function installArgs(pm) {
166
+ if (pm === "npm") return ["install", "--no-audit", "--no-fund", "--loglevel=error"];
167
+ return ["install"];
168
+ }
160
169
 
161
170
  // src/commands/create.ts
162
171
  function toAppName(dir) {
@@ -221,30 +230,48 @@ ${devVarsEntries(authProviders)}`
221
230
  if (packageManager === "pnpm" && !inWorkspace) {
222
231
  writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
223
232
  }
233
+ const log = options2.log ?? ((message) => console.log(message));
224
234
  if (options2.install !== false) {
225
- const code = run(packageManager, ["install"], dir);
235
+ log(`${pc.green("\u2714")} Wrote the app to ${relative(process.cwd(), dir) || "."}`);
236
+ log(`${pc.cyan("\u25CF")} Installing dependencies with ${packageManager}. The first install downloads the Workers runtime and toolchain, so it can take a few minutes.
237
+ `);
238
+ const started = Date.now();
239
+ const code = run(packageManager, installArgs(packageManager), dir);
226
240
  if (code !== 0) throw new Error(`${packageManager} install failed (exit code ${code}).`);
227
- const typegen = run(packageManager, ["run", "cf-typegen"], dir);
228
- if (typegen !== 0) throw new Error(`wrangler types failed (exit code ${typegen}).`);
241
+ log(`${pc.green("\u2714")} Installed dependencies ${pc.dim(`(${formatDuration(Date.now() - started)})`)}`);
242
+ const typegen = runQuiet(packageManager, ["run", "cf-typegen"], dir);
243
+ if (typegen.code !== 0) {
244
+ process.stderr.write(typegen.output);
245
+ throw new Error(`wrangler types failed (exit code ${typegen.code}).`);
246
+ }
247
+ log(`${pc.green("\u2714")} Generated types for the Cloudflare bindings`);
229
248
  }
230
249
  return { dir, name, packageManager, inWorkspace };
231
250
  }
251
+ function formatDuration(ms) {
252
+ const seconds = Math.round(ms / 1e3);
253
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
254
+ }
232
255
  function printNextSteps(result, installed) {
233
256
  const rel = relative(process.cwd(), result.dir) || ".";
234
257
  const pm = result.packageManager;
258
+ const step = (command, note) => ` ${command.padEnd(30)}${pc.dim(note)}`;
235
259
  console.log(`
236
- ${pc.green("\u2714")} Created ${pc.bold(result.name)} in ${rel}
260
+ ${pc.green("\u2714")} Created ${pc.bold(result.name)}
237
261
  `);
238
262
  console.log("Next steps:");
239
263
  console.log(` cd ${rel}`);
240
264
  if (!installed) console.log(` ${pm} install`);
241
- console.log(` ${pm} run dev`);
265
+ console.log(step(`${pm} run dev`, "start it at http://localhost:3000"));
266
+ console.log(step("npx flare gen resource ...", "add your first resource"));
267
+ console.log(`
268
+ Quickstart: ${pc.cyan("https://flare-docs.codetotech.com/start/quickstart/")}`);
242
269
  }
243
270
 
244
271
  // 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";
272
+ import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
273
+ import { dirname as dirname6, join as join11 } from "path";
274
+ import pc6 from "picocolors";
248
275
 
249
276
  // src/generator/grammar.ts
250
277
  import { camelCase, FILE_CATEGORIES } from "@flaredev/core";
@@ -1231,31 +1258,146 @@ async function loadResources(root) {
1231
1258
  }
1232
1259
 
1233
1260
  // 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";
1261
+ import { spawn as spawn4 } from "child_process";
1262
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1263
+ import { join as join8 } from "path";
1237
1264
 
1238
1265
  // 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";
1266
+ import { spawn as spawn3 } from "child_process";
1267
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1268
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
1269
+ import pc4 from "picocolors";
1270
+
1271
+ // src/tunnel.ts
1272
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
1273
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
1274
+ import { homedir } from "os";
1275
+ import { join as join4 } from "path";
1276
+ import pc2 from "picocolors";
1277
+ var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
1278
+ var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
1279
+ function parseTunnelUrl(output) {
1280
+ return TUNNEL_URL.exec(output)?.[0] ?? null;
1281
+ }
1282
+ function parseLocalUrl(output) {
1283
+ const match = LOCAL_URL.exec(output.replace(/\x1b\[[0-9;]*m/g, ""));
1284
+ return match ? match[0].replace("0.0.0.0", "localhost") : null;
1285
+ }
1286
+ function toLocalUrl(target) {
1287
+ if (!target) return "http://localhost:3000";
1288
+ if (/^\d+$/.test(target.replace(/^:/, ""))) return `http://localhost:${target.replace(/^:/, "")}`;
1289
+ if (/^https?:\/\//.test(target)) return target;
1290
+ throw new Error(`"${target}" isn't a port or URL. Try: flare tunnel 3000`);
1291
+ }
1292
+ function releaseAsset(platform = process.platform, arch = process.arch) {
1293
+ const cpu = { x64: "amd64", arm64: "arm64", ia32: "386", arm: "arm" }[arch];
1294
+ if (platform === "win32" && (cpu === "amd64" || cpu === "386")) return `cloudflared-windows-${cpu}.exe`;
1295
+ if (platform === "linux" && cpu) return `cloudflared-linux-${cpu}`;
1296
+ if (platform === "darwin" && (cpu === "amd64" || cpu === "arm64")) return `cloudflared-darwin-${cpu}.tgz`;
1297
+ throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
1298
+ }
1299
+ var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
1300
+ var cacheDir = () => join4(homedir(), ".flare", "bin");
1301
+ function onPath() {
1302
+ const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
1303
+ return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
1304
+ }
1305
+ async function download(log) {
1306
+ const asset = releaseAsset();
1307
+ const dir = cacheDir();
1308
+ mkdirSync3(dir, { recursive: true });
1309
+ const target = join4(dir, binName);
1310
+ log(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
1311
+ const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
1312
+ if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
1313
+ const bytes = Buffer.from(await response.arrayBuffer());
1314
+ const partial = `${target}.download`;
1315
+ if (asset.endsWith(".tgz")) {
1316
+ writeFileSync4(`${partial}.tgz`, bytes);
1317
+ const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
1318
+ rmSync(`${partial}.tgz`, { force: true });
1319
+ if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
1320
+ } else {
1321
+ writeFileSync4(partial, bytes);
1322
+ renameSync(partial, target);
1323
+ }
1324
+ if (process.platform !== "win32") chmodSync(target, 493);
1325
+ return target;
1326
+ }
1327
+ async function ensureCloudflared(log = console.log) {
1328
+ const found = onPath();
1329
+ if (found) return found;
1330
+ const cached = join4(cacheDir(), binName);
1331
+ if (existsSync5(cached)) return cached;
1332
+ return download(log);
1333
+ }
1334
+ async function openTunnel(localUrl, options2 = {}) {
1335
+ const bin = await ensureCloudflared(options2.log);
1336
+ const child = spawn(bin, ["tunnel", "--no-autoupdate", "--url", localUrl], { stdio: ["ignore", "pipe", "pipe"] });
1337
+ const close = () => {
1338
+ if (child.exitCode === null) child.kill();
1339
+ };
1340
+ process.once("exit", close);
1341
+ const url = await new Promise((resolve6, reject) => {
1342
+ let output = "";
1343
+ const timer = setTimeout(() => {
1344
+ close();
1345
+ reject(new Error(`cloudflared didn't report a tunnel URL within ${(options2.timeoutMs ?? 3e4) / 1e3}s:
1346
+ ${output.slice(-2e3)}`));
1347
+ }, options2.timeoutMs ?? 3e4);
1348
+ const scan = (chunk) => {
1349
+ output += chunk.toString();
1350
+ const found = parseTunnelUrl(output);
1351
+ if (found) {
1352
+ clearTimeout(timer);
1353
+ resolve6(found);
1354
+ }
1355
+ };
1356
+ child.stdout?.on("data", scan);
1357
+ child.stderr?.on("data", scan);
1358
+ child.on("error", (error) => {
1359
+ clearTimeout(timer);
1360
+ reject(error);
1361
+ });
1362
+ child.on("exit", (code) => {
1363
+ clearTimeout(timer);
1364
+ reject(new Error(`cloudflared exited (code ${code}) before the tunnel was ready:
1365
+ ${output.slice(-2e3)}`));
1366
+ });
1367
+ });
1368
+ child.stdout?.resume();
1369
+ child.stderr?.resume();
1370
+ return { url, process: child, close };
1371
+ }
1372
+ function tunnelBanner(publicUrl, localUrl) {
1373
+ const line = "\u2500".repeat(Math.max(publicUrl.length, localUrl.length) + 14);
1374
+ return [
1375
+ "",
1376
+ pc2.cyan(line),
1377
+ ` ${pc2.bold("Public URL")} ${pc2.green(pc2.bold(publicUrl))}`,
1378
+ ` ${pc2.dim("Forwarding")} ${pc2.dim(localUrl)}`,
1379
+ pc2.cyan(line),
1380
+ pc2.dim(" Anyone with this link can reach your local server. Stop with Ctrl+C."),
1381
+ ""
1382
+ ].join("\n");
1383
+ }
1242
1384
 
1243
1385
  // src/commands/deploy.ts
1244
1386
  import { randomBytes as randomBytes2 } from "crypto";
1245
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1246
- import { join as join5 } from "path";
1387
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
1388
+ import { join as join6 } from "path";
1247
1389
  import { createZoneClient } from "@flaredev/core/security";
1248
1390
  import { createJiti as createJiti2 } from "jiti";
1249
- import pc2 from "picocolors";
1391
+ import pc3 from "picocolors";
1250
1392
 
1251
1393
  // 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";
1394
+ import { spawn as spawn2 } from "child_process";
1395
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1396
+ import { join as join5 } from "path";
1255
1397
  import { parse as parseJsonc } from "jsonc-parser";
1256
1398
  function runWrangler(wranglerBin, args, cwd, options2 = {}) {
1257
1399
  return new Promise((resolve6, reject) => {
1258
- const child = spawn(process.execPath, [wranglerBin, ...args], {
1400
+ const child = spawn2(process.execPath, [wranglerBin, ...args], {
1259
1401
  cwd,
1260
1402
  stdio: [options2.stdin !== void 0 ? "pipe" : "ignore", options2.capture ? "pipe" : "inherit", options2.capture ? "pipe" : "inherit"]
1261
1403
  });
@@ -1276,8 +1418,8 @@ ${output.trim()}`);
1276
1418
  return JSON.parse(output.slice(start, end + 1));
1277
1419
  }
1278
1420
  function readD1Databases(appRoot) {
1279
- const path = join4(appRoot, "wrangler.jsonc");
1280
- if (!existsSync5(path)) return [];
1421
+ const path = join5(appRoot, "wrangler.jsonc");
1422
+ if (!existsSync6(path)) return [];
1281
1423
  const config = parseJsonc(readFileSync3(path, "utf8"));
1282
1424
  return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
1283
1425
  }
@@ -1317,15 +1459,15 @@ function declaredSecretNames(devVarsExample) {
1317
1459
  return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
1318
1460
  }
1319
1461
  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 });
1462
+ const path = join6(appRoot, "security.config.ts");
1463
+ if (!existsSync7(path)) return null;
1464
+ const jiti = createJiti2(join6(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1323
1465
  const mod = await jiti.import(path);
1324
1466
  if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
1325
1467
  return mod.default;
1326
1468
  }
1327
1469
  async function provisionZoneSecurity(appRoot, config, credentials, options2) {
1328
- const app = JSON.parse(readFileSync4(join5(appRoot, "package.json"), "utf8")).name ?? "app";
1470
+ const app = JSON.parse(readFileSync4(join6(appRoot, "package.json"), "utf8")).name ?? "app";
1329
1471
  const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
1330
1472
  if (config.zone?.rateLimits) {
1331
1473
  const result = await zone.syncRateLimits(config.zone.rateLimits);
@@ -1344,8 +1486,8 @@ async function runDeploy(args, context3) {
1344
1486
  const log = context3.log ?? ((message) => console.log(message));
1345
1487
  const envArgs = plan.env ? ["--env", plan.env] : [];
1346
1488
  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")));
1489
+ ${pc3.bold(pc3.cyan("flare"))} ${message}`);
1490
+ const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync7(join6(appRoot, db.migrations_dir ?? "migrations")));
1349
1491
  const migrateAfterDeploy = [];
1350
1492
  const migrate2 = async (db) => {
1351
1493
  step(`Applying migrations to ${db.database_name} (remote)`);
@@ -1404,8 +1546,8 @@ ${put.output.trim()}`);
1404
1546
  existing.add(name);
1405
1547
  }
1406
1548
  }
1407
- const examplePath = join5(appRoot, ".dev.vars.example");
1408
- const optional = existsSync6(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1549
+ const examplePath = join6(appRoot, ".dev.vars.example");
1550
+ const optional = existsSync7(examplePath) ? declaredSecretNames(readFileSync4(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
1409
1551
  if (optional.length > 0) {
1410
1552
  step("Optional secrets not set in production (features using them stay off):");
1411
1553
  for (const name of optional) log(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
@@ -1416,10 +1558,10 @@ ${put.output.trim()}`);
1416
1558
 
1417
1559
  // src/commands/run.ts
1418
1560
  var DELEGATED_COMMANDS = {
1419
- dev: { description: "Start the vinext dev server", pkg: "vinext", bin: "vinext", args: ["dev"] },
1561
+ dev: { description: "Start the vinext dev server (--tunnel: also share it on a public URL)", pkg: "vinext", bin: "vinext", args: ["dev"] },
1420
1562
  build: { description: "Build for production (vinext build)", pkg: "vinext", bin: "vinext", args: ["build"] },
1421
1563
  start: {
1422
- description: "Serve the production build locally in workerd (builds first if needed)",
1564
+ description: "Serve the production build locally in workerd, building first if needed (--tunnel: also share it on a public URL)",
1423
1565
  pkg: "wrangler",
1424
1566
  bin: "wrangler",
1425
1567
  // --persist-to shares local D1/R2 state with `flare dev` and local migrations.
@@ -1440,23 +1582,23 @@ function findAppRoot(cwd) {
1440
1582
  while (dir) {
1441
1583
  const root = findUp("package.json", dir);
1442
1584
  if (!root) break;
1443
- const pkg = JSON.parse(readFileSync5(join6(root, "package.json"), "utf8"));
1585
+ const pkg = JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
1444
1586
  if (pkg.dependencies?.vinext || pkg.devDependencies?.vinext) return root;
1445
1587
  const parent = dirname4(root);
1446
1588
  dir = parent === root ? void 0 : parent;
1447
1589
  }
1448
1590
  throw new Error("Not inside a Flare app (no package.json depending on vinext found). Run this from your app directory.");
1449
1591
  }
1450
- function resolveBin(appRoot, pkgName, binName) {
1451
- const pkgDir = findUp(join6("node_modules", pkgName, "package.json"), appRoot);
1592
+ function resolveBin(appRoot, pkgName, binName2) {
1593
+ const pkgDir = findUp(join7("node_modules", pkgName, "package.json"), appRoot);
1452
1594
  if (!pkgDir) {
1453
1595
  throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
1454
1596
  }
1455
- const manifestPath = join6(pkgDir, "node_modules", pkgName, "package.json");
1597
+ const manifestPath = join7(pkgDir, "node_modules", pkgName, "package.json");
1456
1598
  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);
1599
+ const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
1600
+ if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
1601
+ return join7(dirname4(manifestPath), relativeBin);
1460
1602
  }
1461
1603
  function delegatedArgv(appRoot, command, forwarded) {
1462
1604
  const spec = DELEGATED_COMMANDS[command];
@@ -1464,15 +1606,60 @@ function delegatedArgv(appRoot, command, forwarded) {
1464
1606
  }
1465
1607
  function runNode(argv, cwd) {
1466
1608
  return new Promise((resolvePromise, reject) => {
1467
- const child = spawn2(process.execPath, argv, { cwd, stdio: "inherit" });
1609
+ const child = spawn3(process.execPath, argv, { cwd, stdio: "inherit" });
1468
1610
  child.on("error", reject);
1469
1611
  child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
1470
1612
  });
1471
1613
  }
1614
+ function runNodeWithTunnel(argv, cwd, opened) {
1615
+ return new Promise((resolvePromise, reject) => {
1616
+ const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
1617
+ let tunnel = opened;
1618
+ let seen = "";
1619
+ let announced = false;
1620
+ const watch = (chunk, out) => {
1621
+ out.write(chunk);
1622
+ if (announced) return;
1623
+ seen += chunk.toString();
1624
+ const local = parseLocalUrl(seen);
1625
+ if (!local) return;
1626
+ announced = true;
1627
+ if (opened) {
1628
+ process.stdout.write(tunnelBanner(opened.url, local));
1629
+ return;
1630
+ }
1631
+ openTunnel(local).then(
1632
+ (fresh) => {
1633
+ tunnel = fresh;
1634
+ if (child.exitCode !== null) fresh.close();
1635
+ else process.stdout.write(tunnelBanner(fresh.url, local));
1636
+ },
1637
+ (error) => process.stderr.write(pc4.red(`
1638
+ The tunnel couldn't start: ${error.message}
1639
+ The server is still running locally.
1640
+ `))
1641
+ );
1642
+ };
1643
+ child.stdout.on("data", (chunk) => watch(chunk, process.stdout));
1644
+ child.stderr.on("data", (chunk) => watch(chunk, process.stderr));
1645
+ child.on("error", reject);
1646
+ child.on("exit", (code, signal) => {
1647
+ tunnel?.close();
1648
+ resolvePromise(code ?? (signal ? 1 : 0));
1649
+ });
1650
+ });
1651
+ }
1652
+ function portArg(args) {
1653
+ const index = args.findIndex((arg) => arg === "--port" || arg === "-p");
1654
+ if (index !== -1) return args[index + 1];
1655
+ return args.find((arg) => arg.startsWith("--port="))?.slice("--port=".length);
1656
+ }
1472
1657
  async function runDelegated(command, forwarded, cwd = process.cwd()) {
1473
1658
  const appRoot = findAppRoot(cwd);
1659
+ const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
1660
+ if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
1474
1661
  const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
1475
- if (command === "start" && !wantsHelp && !existsSync7(join6(appRoot, "dist/server/wrangler.json"))) {
1662
+ if (command === "start" && !wantsHelp && !existsSync8(join7(appRoot, "dist/server/wrangler.json"))) {
1476
1663
  console.log("No production build found; running `flare build` first.\n");
1477
1664
  const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
1478
1665
  if (code !== 0) return code;
@@ -1484,6 +1671,12 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
1484
1671
  deploy: (args) => runNode(delegatedArgv(appRoot, "deploy", args), appRoot)
1485
1672
  });
1486
1673
  }
1674
+ if (tunnel && !wantsHelp && command === "start") {
1675
+ const opened = await openTunnel(`http://127.0.0.1:${portArg(forwarded) ?? "8787"}`);
1676
+ const origin = ["--local-upstream", new URL(opened.url).host, "--upstream-protocol", "https"];
1677
+ return runNodeWithTunnel(delegatedArgv(appRoot, command, [...forwarded, ...origin]), appRoot, opened);
1678
+ }
1679
+ if (tunnel && !wantsHelp) return runNodeWithTunnel(delegatedArgv(appRoot, command, forwarded), appRoot);
1487
1680
  return runNode(delegatedArgv(appRoot, command, forwarded), appRoot);
1488
1681
  }
1489
1682
 
@@ -1516,14 +1709,14 @@ function snapshotColumns(snapshot) {
1516
1709
  }
1517
1710
 
1518
1711
  // src/generator/migrations.ts
1519
- var listSql = (dir) => existsSync8(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1712
+ var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
1520
1713
  async function generateSchemaMigration(appRoot, name, options2 = {}) {
1521
- const dir = join7(appRoot, "migrations");
1714
+ const dir = join8(appRoot, "migrations");
1522
1715
  const before = new Set(listSql(dir));
1523
1716
  const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
1524
1717
  const { code, output } = await new Promise((resolve6, reject) => {
1525
1718
  const args = ["generate", "--name", name, ...options2.custom ? ["--custom"] : []];
1526
- const child = spawn3(process.execPath, [bin, ...args], {
1719
+ const child = spawn4(process.execPath, [bin, ...args], {
1527
1720
  cwd: appRoot,
1528
1721
  stdio: ["ignore", "pipe", "pipe"]
1529
1722
  });
@@ -1546,10 +1739,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1546
1739
  for (const file of created) {
1547
1740
  const previous = previousSnapshot(dir, file);
1548
1741
  if (!previous) continue;
1549
- const path = join7(dir, file);
1742
+ const path = join8(dir, file);
1550
1743
  const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
1551
1744
  if (changes.length) {
1552
- writeFileSync4(path, sql2);
1745
+ writeFileSync5(path, sql2);
1553
1746
  repairs.push(...changes.map((change) => `${file}: ${change}`));
1554
1747
  }
1555
1748
  }
@@ -1557,32 +1750,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
1557
1750
  }
1558
1751
  function previousSnapshot(dir, migrationFile) {
1559
1752
  const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
1560
- const metaDir = join7(dir, "meta");
1561
- if (!prefix || !existsSync8(metaDir)) return void 0;
1753
+ const metaDir = join8(dir, "meta");
1754
+ if (!prefix || !existsSync9(metaDir)) return void 0;
1562
1755
  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;
1756
+ return earlier ? JSON.parse(readFileSync6(join8(metaDir, earlier), "utf8")) : void 0;
1564
1757
  }
1565
1758
 
1566
1759
  // 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";
1760
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
1761
+ import { dirname as dirname5, join as join10 } from "path";
1762
+ import pc5 from "picocolors";
1570
1763
 
1571
1764
  // src/generator/policy.ts
1572
- import { existsSync as existsSync9, readdirSync as readdirSync5 } from "fs";
1573
- import { join as join8, resolve as resolve5 } from "path";
1765
+ import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1766
+ import { join as join9, resolve as resolve5 } from "path";
1574
1767
  import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
1575
1768
  import { createJiti as createJiti3 } from "jiti";
1576
1769
  var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
1577
1770
  var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
1578
1771
  async function loadPolicies(root) {
1579
1772
  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 });
1773
+ const dir = join9(appRoot, "policies");
1774
+ if (!existsSync10(dir)) return [];
1775
+ const jiti = createJiti3(join9(appRoot, "package.json"), { moduleCache: false, fsCache: false });
1583
1776
  const loaded = [];
1584
1777
  for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
1585
- const mod = await jiti.import(join8(dir, file));
1778
+ const mod = await jiti.import(join9(dir, file));
1586
1779
  const policy = mod.default;
1587
1780
  if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
1588
1781
  throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
@@ -1923,7 +2116,7 @@ function planFiles(all, policies) {
1923
2116
  }
1924
2117
  function applyPlan(appRoot, plan, options2 = {}) {
1925
2118
  return plan.map(({ path, content, header }) => {
1926
- const absolute = join9(appRoot, path);
2119
+ const absolute = join10(appRoot, path);
1927
2120
  const status = inspectGenerated(absolute, content);
1928
2121
  if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
1929
2122
  if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
@@ -1942,37 +2135,37 @@ function applyPlan(appRoot, plan, options2 = {}) {
1942
2135
  function findOrphans(appRoot, all) {
1943
2136
  const planned = new Set(planFiles(all).map((file) => file.path));
1944
2137
  const candidates = [];
1945
- const list2 = (dir, match) => existsSync10(join9(appRoot, dir)) ? readdirSync6(join9(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
2138
+ const list2 = (dir, match) => existsSync11(join10(appRoot, dir)) ? readdirSync6(join10(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
1946
2139
  candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
1947
2140
  candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
1948
2141
  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);
2142
+ for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
1950
2143
  }
1951
2144
  for (const dir of list2("app/admin", () => true)) {
1952
2145
  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);
2146
+ if (existsSync11(join10(appRoot, file))) candidates.push(file);
1954
2147
  }
1955
2148
  }
1956
2149
  const orphans = [];
1957
2150
  for (const path of candidates) {
1958
- const match = HEADER_PATTERN.exec(readFileSync7(join9(appRoot, path), "utf8"));
2151
+ const match = HEADER_PATTERN.exec(readFileSync7(join10(appRoot, path), "utf8"));
1959
2152
  if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
1960
2153
  }
1961
2154
  return orphans;
1962
2155
  }
1963
2156
  function ensureSupportFiles(appRoot, log) {
1964
2157
  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}`);
2158
+ const target = join10(appRoot, path);
2159
+ if (existsSync11(target)) continue;
2160
+ mkdirSync4(dirname5(target), { recursive: true });
2161
+ copyFileSync(join10(templatesDir, "app", path), target);
2162
+ log(`${pc5.green("create".padEnd(9))} ${path}`);
1970
2163
  }
1971
2164
  }
1972
2165
  function logResults(results, log, options2 = {}) {
1973
2166
  for (const { path, outcome } of results) {
1974
2167
  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");
2168
+ 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
2169
  log(`${label.padEnd(18)} ${path}`);
1977
2170
  }
1978
2171
  }
@@ -1993,14 +2186,14 @@ function billingFiles(appName) {
1993
2186
  }
1994
2187
  var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
1995
2188
  function appendVars(appRoot, target) {
1996
- const path = join10(appRoot, target);
1997
- const text = existsSync11(path) ? readFileSync8(path, "utf8") : "";
2189
+ const path = join11(appRoot, target);
2190
+ const text = existsSync12(path) ? readFileSync8(path, "utf8") : "";
1998
2191
  if (/STRIPE_SECRET_KEY/.test(text)) return false;
1999
2192
  appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
2000
2193
  return true;
2001
2194
  }
2002
2195
  function addStripeDependency(appRoot) {
2003
- const path = join10(appRoot, "package.json");
2196
+ const path = join11(appRoot, "package.json");
2004
2197
  const pkg = JSON.parse(readFileSync8(path, "utf8"));
2005
2198
  pkg.dependencies ??= {};
2006
2199
  if (pkg.dependencies.stripe) return false;
@@ -2010,18 +2203,18 @@ function addStripeDependency(appRoot) {
2010
2203
  }
2011
2204
  function writeBillingResource(appRoot, name, force, log) {
2012
2205
  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}`);
2206
+ const path = join11(appRoot, relative2);
2207
+ if (!existsSync12(path)) {
2208
+ mkdirSync5(dirname6(path), { recursive: true });
2209
+ writeFileSync6(path, renderBillingDescriptor(name));
2210
+ log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2018
2211
  return;
2019
2212
  }
2020
2213
  const parts = splitMarkers(readFileSync8(path, "utf8"));
2021
2214
  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
2215
  const merged = mergeBillingFields(name, parts.block);
2023
2216
  if (!merged) {
2024
- log(`${pc4.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2217
+ log(`${pc6.dim("identical".padEnd(9))} ${relative2} (has every billing field)`);
2025
2218
  return;
2026
2219
  }
2027
2220
  try {
@@ -2032,7 +2225,7 @@ function writeBillingResource(appRoot, name, force, log) {
2032
2225
  }
2033
2226
  throw error;
2034
2227
  }
2035
- log(`${pc4.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2228
+ log(`${pc6.yellow("update".padEnd(9))} ${relative2} (added billing fields, kept yours)`);
2036
2229
  }
2037
2230
  async function genBilling(options2 = {}) {
2038
2231
  const log = options2.log ?? ((message) => console.log(message));
@@ -2041,38 +2234,38 @@ async function genBilling(options2 = {}) {
2041
2234
  for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log);
2042
2235
  for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
2043
2236
  const relative2 = policyPath(name);
2044
- if (existsSync11(join10(appRoot, relative2))) {
2045
- log(`${pc4.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2237
+ if (existsSync12(join11(appRoot, relative2))) {
2238
+ log(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2046
2239
  continue;
2047
2240
  }
2048
- mkdirSync4(join10(appRoot, "policies"), { recursive: true });
2049
- writeFileSync5(join10(appRoot, relative2), renderPolicy(name, roles));
2050
- log(`${pc4.green("create".padEnd(9))} ${relative2}`);
2241
+ mkdirSync5(join11(appRoot, "policies"), { recursive: true });
2242
+ writeFileSync6(join11(appRoot, relative2), renderPolicy(name, roles));
2243
+ log(`${pc6.green("create".padEnd(9))} ${relative2}`);
2051
2244
  }
2052
2245
  const files = [];
2053
- const appName = JSON.parse(readFileSync8(join10(appRoot, "package.json"), "utf8")).name ?? "app";
2246
+ const appName = JSON.parse(readFileSync8(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2054
2247
  for (const { path, content } of billingFiles(appName)) {
2055
- const outcome = writeGenerated(join10(appRoot, path), content, { header: billingHeader(), force: options2.force });
2248
+ const outcome = writeGenerated(join11(appRoot, path), content, { header: billingHeader(), force: options2.force });
2056
2249
  files.push({ path, status: "missing", outcome });
2057
- const label = outcome === "create" ? pc4.green("create") : outcome === "update" ? pc4.yellow("update") : pc4.dim("identical");
2250
+ const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
2058
2251
  log(`${label.padEnd(18)} ${path}`);
2059
2252
  }
2060
2253
  for (const path of RETIRED_FILES) {
2061
- const absolute = join10(appRoot, path);
2062
- if (!existsSync11(absolute)) continue;
2254
+ const absolute = join11(appRoot, path);
2255
+ if (!existsSync12(absolute)) continue;
2063
2256
  const parts = splitMarkers(readFileSync8(absolute, "utf8"));
2064
2257
  const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
2065
2258
  if (untouched) {
2066
2259
  unlinkSync(absolute);
2067
- log(`${pc4.red("remove".padEnd(9))} ${path} (no longer generated)`);
2260
+ log(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
2068
2261
  } else {
2069
- log(pc4.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2262
+ log(pc6.yellow(`${path} is no longer generated but has your changes; remove it when you're done with it.`));
2070
2263
  }
2071
2264
  }
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`);
2265
+ if (appendVars(appRoot, ".dev.vars.example")) log(`${pc6.green("append".padEnd(9))} .dev.vars.example`);
2266
+ if (appendVars(appRoot, ".dev.vars")) log(`${pc6.green("append".padEnd(9))} .dev.vars`);
2074
2267
  const addedStripe = addStripeDependency(appRoot);
2075
- if (addedStripe) log(`${pc4.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2268
+ if (addedStripe) log(`${pc6.yellow("update".padEnd(9))} package.json (stripe@${STRIPE_DEP})`);
2076
2269
  if (!options2.skipInstall) {
2077
2270
  const pm = detectPackageManager();
2078
2271
  const code = run(pm, ["install"], appRoot);
@@ -2086,27 +2279,27 @@ async function genBilling(options2 = {}) {
2086
2279
  if (!options2.skipMigration) {
2087
2280
  const result = await generateSchemaMigration(appRoot, "billing");
2088
2281
  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}`));
2282
+ for (const file of migrations) log(`${pc6.green("create".padEnd(9))} ${file}`);
2283
+ for (const repair of result.repairs) log(pc6.dim(`repaired ${repair}`));
2091
2284
  }
2092
2285
  log(
2093
2286
  [
2094
2287
  "",
2095
2288
  `Next:`,
2096
2289
  ` 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")}.`
2290
+ ` 2. ${pc6.bold("flare migrate")} to create the billing tables.`,
2291
+ ` 3. Create one Product per plan in Stripe with metadata flare_app=${appName}, then ${pc6.bold("flare billing:sync-plans")}.`,
2292
+ ` 4. ${pc6.bold("stripe listen --forward-to localhost:8787/api/webhooks/stripe")} and put its signing secret in STRIPE_WEBHOOK_SECRET.`,
2293
+ ` 5. ${pc6.bold("flare start")} and open ${pc6.bold("/dashboard/billing")}.`
2101
2294
  ].join("\n")
2102
2295
  );
2103
2296
  return { files, migrations, addedStripe };
2104
2297
  }
2105
2298
 
2106
2299
  // 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";
2300
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2301
+ import { dirname as dirname7, join as join12 } from "path";
2302
+ import pc7 from "picocolors";
2110
2303
 
2111
2304
  // src/generator/security.ts
2112
2305
  import { applyEdits, modify, parse as parseJsonc2 } from "jsonc-parser";
@@ -2633,49 +2826,49 @@ function securityFiles(appName) {
2633
2826
  }
2634
2827
  var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
2635
2828
  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)`);
2829
+ const path = join12(appRoot, relative2);
2830
+ if (existsSync13(path)) {
2831
+ log(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
2639
2832
  return;
2640
2833
  }
2641
- mkdirSync5(dirname7(path), { recursive: true });
2642
- writeFileSync6(path, content);
2643
- log(`${pc5.green("create".padEnd(9))} ${relative2}`);
2834
+ mkdirSync6(dirname7(path), { recursive: true });
2835
+ writeFileSync7(path, content);
2836
+ log(`${pc7.green("create".padEnd(9))} ${relative2}`);
2644
2837
  }
2645
2838
  async function genSecurity(options2 = {}) {
2646
2839
  const log = options2.log ?? ((message) => console.log(message));
2647
2840
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2648
- const appName = JSON.parse(readFileSync9(join11(appRoot, "package.json"), "utf8")).name ?? "app";
2841
+ const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
2649
2842
  writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log);
2650
2843
  writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log);
2651
2844
  writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log);
2652
2845
  const files = [];
2653
2846
  const write = (path, content, header) => {
2654
- const absolute = join11(appRoot, path);
2655
- if (path === "lib/security.ts" && existsSync12(absolute)) {
2847
+ const absolute = join12(appRoot, path);
2848
+ if (path === "lib/security.ts" && existsSync13(absolute)) {
2656
2849
  const source = readFileSync9(absolute, "utf8");
2657
- if (PASS_THROUGH_HEADER.test(source)) writeFileSync6(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
2850
+ if (PASS_THROUGH_HEADER.test(source)) writeFileSync7(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
2658
2851
  }
2659
2852
  const outcome = writeGenerated(absolute, content, { header, force: options2.force });
2660
2853
  files.push({ path, status: "missing", outcome });
2661
- const label = outcome === "create" ? pc5.green("create") : outcome === "update" ? pc5.yellow("update") : pc5.dim("identical");
2854
+ const label = outcome === "create" ? pc7.green("create") : outcome === "update" ? pc7.yellow("update") : pc7.dim("identical");
2662
2855
  log(`${label.padEnd(18)} ${path}`);
2663
2856
  };
2664
2857
  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");
2858
+ const navPath = join12(appRoot, "lib/admin-nav.ts");
2859
+ if (existsSync13(navPath) && splitMarkers(readFileSync9(navPath, "utf8"))) write("lib/admin-nav.ts", renderAdminNavBlock());
2860
+ else log(pc7.yellow(`lib/admin-nav.ts not found: link to /admin/security from your admin sidebar yourself.`));
2861
+ const wranglerPath = join12(appRoot, "wrangler.jsonc");
2669
2862
  let bindings = [];
2670
- if (existsSync12(wranglerPath)) {
2863
+ if (existsSync13(wranglerPath)) {
2671
2864
  const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
2672
2865
  bindings = added;
2673
2866
  if (added.length) {
2674
- writeFileSync6(wranglerPath, text);
2675
- for (const line of added) log(`${pc5.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2867
+ writeFileSync7(wranglerPath, text);
2868
+ for (const line of added) log(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
2676
2869
  }
2677
2870
  } else {
2678
- log(pc5.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2871
+ log(pc7.yellow("wrangler.jsonc not found: add the FLARE_SECURITY, FLARE_SECURITY_MONITOR and FLARE_RATE_LIMIT bindings yourself."));
2679
2872
  }
2680
2873
  const all = await loadResources(appRoot);
2681
2874
  ensureSupportFiles(appRoot, log);
@@ -2684,17 +2877,17 @@ async function genSecurity(options2 = {}) {
2684
2877
  if (!options2.skipMigration) {
2685
2878
  const result = await generateSchemaMigration(appRoot, "security");
2686
2879
  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}`));
2880
+ for (const file of migrations) log(`${pc7.green("create".padEnd(9))} ${file}`);
2881
+ for (const repair of result.repairs) log(pc7.dim(`repaired ${repair}`));
2689
2882
  }
2690
2883
  log(
2691
2884
  [
2692
2885
  "",
2693
2886
  `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.`,
2887
+ ` 1. ${pc7.bold("flare migrate")} to create the security_events table.`,
2888
+ ` 2. ${pc7.bold("flare dev")}, then open ${pc7.bold("/admin/security")}. Tune detectors in security.config.ts.`,
2696
2889
  ` 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`,
2890
+ ` one zone with Zone WAF Edit, Firewall Services Edit, Analytics Read, Zone Read). ${pc7.bold("flare deploy")} pushes the`,
2698
2891
  ` zone rules and uploads both as secrets.`
2699
2892
  ].join("\n")
2700
2893
  );
@@ -2702,10 +2895,10 @@ async function genSecurity(options2 = {}) {
2702
2895
  }
2703
2896
 
2704
2897
  // 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";
2898
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2899
+ import { dirname as dirname8, join as join13 } from "path";
2707
2900
  import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
2708
- import pc6 from "picocolors";
2901
+ import pc8 from "picocolors";
2709
2902
  function validateFields(name, fields, others, log) {
2710
2903
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
2711
2904
  for (const field of fields) {
@@ -2716,7 +2909,7 @@ function validateFields(name, fields, others, log) {
2716
2909
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
2717
2910
  const graph = relationGraph2([...others, candidate]);
2718
2911
  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).`));
2912
+ log(pc8.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
2720
2913
  }
2721
2914
  }
2722
2915
  async function genResource(rawName, options2) {
@@ -2724,8 +2917,8 @@ async function genResource(rawName, options2) {
2724
2917
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2725
2918
  const name = resourceName(rawName);
2726
2919
  const relativePath = descriptorPath(name);
2727
- const path = join12(appRoot, relativePath);
2728
- const exists = existsSync13(path);
2920
+ const path = join13(appRoot, relativePath);
2921
+ const exists = existsSync14(path);
2729
2922
  if (!exists && !options2.fields) {
2730
2923
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
2731
2924
  }
@@ -2735,13 +2928,13 @@ async function genResource(rawName, options2) {
2735
2928
  const fields = parseFields(options2.fields);
2736
2929
  validateFields(name, fields, others, log);
2737
2930
  if (!exists) {
2738
- mkdirSync6(dirname8(path), { recursive: true });
2739
- writeFileSync7(path, renderDescriptor(name, fields));
2740
- log(`${pc6.green("create".padEnd(9))} ${relativePath}`);
2931
+ mkdirSync7(dirname8(path), { recursive: true });
2932
+ writeFileSync8(path, renderDescriptor(name, fields));
2933
+ log(`${pc8.green("create".padEnd(9))} ${relativePath}`);
2741
2934
  } else {
2742
2935
  try {
2743
2936
  const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
2744
- log(`${(outcome === "identical" ? pc6.dim : pc6.yellow)(outcome.padEnd(9))} ${relativePath}`);
2937
+ log(`${(outcome === "identical" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relativePath}`);
2745
2938
  } catch (error) {
2746
2939
  if (error instanceof DriftError) {
2747
2940
  throw new Error(
@@ -2767,28 +2960,28 @@ async function genResource(rawName, options2) {
2767
2960
  }
2768
2961
  const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
2769
2962
  if (skipped.length) {
2770
- log(pc6.yellow(`
2963
+ log(pc8.yellow(`
2771
2964
  Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
2772
2965
  }
2773
2966
  let migrations = [];
2774
2967
  if (!options2.skipMigration) {
2775
2968
  const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
2776
2969
  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}`));
2970
+ for (const file of migrations) log(`${pc8.green("create".padEnd(9))} ${file}`);
2971
+ for (const repair of result.repairs) log(pc8.dim(`repaired ${repair}`));
2779
2972
  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."));
2973
+ Next: ${pc8.bold("flare migrate")} to apply it locally.`);
2974
+ else if (exists) log(pc8.dim("\nNo table changes, so no migration."));
2782
2975
  }
2783
2976
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
2784
2977
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
2785
2978
  }
2786
2979
 
2787
2980
  // 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";
2981
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
2982
+ import { basename as basename2, join as join14 } from "path";
2790
2983
  import { snakeCase } from "@flaredev/core";
2791
- import pc7 from "picocolors";
2984
+ import pc9 from "picocolors";
2792
2985
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
2793
2986
  -- Write SQL that undoes the up migration, e.g.:
2794
2987
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
@@ -2804,31 +2997,31 @@ async function genMigration(rawName, options2 = {}) {
2804
2997
  log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
2805
2998
  return [];
2806
2999
  }
2807
- for (const file of files) log(`${pc7.green("create".padEnd(9))} ${file}`);
2808
- for (const repair of repairs) log(pc7.dim(`repaired ${repair}`));
3000
+ for (const file of files) log(`${pc9.green("create".padEnd(9))} ${file}`);
3001
+ for (const repair of repairs) log(pc9.dim(`repaired ${repair}`));
2809
3002
  if (!options2.fromSchema) {
2810
- const downDir = join13(appRoot, "migrations", "down");
2811
- mkdirSync7(downDir, { recursive: true });
3003
+ const downDir = join14(appRoot, "migrations", "down");
3004
+ mkdirSync8(downDir, { recursive: true });
2812
3005
  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)}`);
3006
+ const down = join14(downDir, basename2(file));
3007
+ if (existsSync15(down)) continue;
3008
+ writeFileSync9(down, DOWN_TEMPLATE(basename2(file)));
3009
+ log(`${pc9.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
2817
3010
  }
2818
3011
  log(`
2819
- Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc7.bold("flare migrate")}.`);
3012
+ Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc9.bold("flare migrate")}.`);
2820
3013
  } else {
2821
3014
  log(`
2822
- Next: ${pc7.bold("flare migrate")}.`);
3015
+ Next: ${pc9.bold("flare migrate")}.`);
2823
3016
  }
2824
3017
  return files;
2825
3018
  }
2826
3019
 
2827
3020
  // 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";
3021
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
3022
+ import { dirname as dirname9, join as join15 } from "path";
2830
3023
  import { definePolicy } from "@flaredev/core";
2831
- import pc8 from "picocolors";
3024
+ import pc10 from "picocolors";
2832
3025
  function parseRoles(input, what = "--roles") {
2833
3026
  const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
2834
3027
  if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
@@ -2844,8 +3037,8 @@ async function genPolicy(rawName, options2) {
2844
3037
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
2845
3038
  }
2846
3039
  const relative2 = policyPath(name);
2847
- const path = join14(appRoot, relative2);
2848
- const exists = existsSync15(path);
3040
+ const path = join15(appRoot, relative2);
3041
+ const exists = existsSync16(path);
2849
3042
  let roles;
2850
3043
  let deleteRoles;
2851
3044
  if (options2.roles || !exists) {
@@ -2856,13 +3049,13 @@ async function genPolicy(rawName, options2) {
2856
3049
  if (roles && deleteRoles) {
2857
3050
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
2858
3051
  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}`);
3052
+ mkdirSync9(dirname9(path), { recursive: true });
3053
+ writeFileSync10(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3054
+ log(`${pc10.green("create".padEnd(9))} ${relative2}`);
2862
3055
  } else {
2863
3056
  try {
2864
3057
  const outcome = writeGenerated(path, block, { force: options2.force });
2865
- log(`${(outcome === "identical" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relative2}`);
3058
+ log(`${(outcome === "identical" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relative2}`);
2866
3059
  } catch (error) {
2867
3060
  if (error instanceof DriftError) {
2868
3061
  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 +3067,15 @@ async function genPolicy(rawName, options2) {
2874
3067
  const policies = await loadPolicies(appRoot);
2875
3068
  logResults(applyPlan(appRoot, planFiles(resources, policies)), log);
2876
3069
  log(`
2877
- ${pc8.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
3070
+ ${pc10.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
2878
3071
  return { name, path: relative2 };
2879
3072
  }
2880
3073
 
2881
3074
  // src/commands/migrate.ts
2882
- import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync10, rmSync, writeFileSync as writeFileSync10 } from "fs";
3075
+ import { existsSync as existsSync17, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync11 } from "fs";
2883
3076
  import { tmpdir } from "os";
2884
- import { join as join15 } from "path";
2885
- import pc9 from "picocolors";
3077
+ import { join as join16 } from "path";
3078
+ import pc11 from "picocolors";
2886
3079
 
2887
3080
  // src/generator/down.ts
2888
3081
  var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
@@ -2999,7 +3192,7 @@ async function rollback(options2 = {}) {
2999
3192
  const steps = options2.steps ?? 1;
3000
3193
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3001
3194
  const { appRoot, db, target, where, wrangler, log } = context(options2);
3002
- const migrationsDir = join15(appRoot, db.migrations_dir ?? "migrations");
3195
+ const migrationsDir = join16(appRoot, db.migrations_dir ?? "migrations");
3003
3196
  const applied = await appliedMigrations(options2, steps);
3004
3197
  if (applied.length === 0) {
3005
3198
  log("Nothing to roll back.");
@@ -3008,8 +3201,8 @@ async function rollback(options2 = {}) {
3008
3201
  const plan = [];
3009
3202
  for (const { name } of applied) {
3010
3203
  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)) {
3204
+ const handWritten = join16(migrationsDir, "down", name);
3205
+ if (existsSync17(handWritten)) {
3013
3206
  const sql2 = readFileSync10(handWritten, "utf8");
3014
3207
  if (splitStatements(sql2).length === 0) {
3015
3208
  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 +3210,8 @@ async function rollback(options2 = {}) {
3017
3210
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3018
3211
  continue;
3019
3212
  }
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.`);
3213
+ const upPath = join16(migrationsDir, name);
3214
+ if (!existsSync17(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3022
3215
  const down = deriveDown(readFileSync10(upPath, "utf8"));
3023
3216
  if (!down.reversible) {
3024
3217
  throw new Error(
@@ -3028,37 +3221,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3028
3221
  }
3029
3222
  plan.push({ name, sql: down.sql, source: "derived" });
3030
3223
  }
3031
- log(`${pc9.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3224
+ log(`${pc11.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
3032
3225
  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}`));
3226
+ log(` ${step.name} ${pc11.dim(`(${step.source})`)}`);
3227
+ for (const line of step.sql.trim().split("\n")) log(pc11.dim(` ${line}`));
3035
3228
  }
3036
3229
  if (options2.remote && !options2.yes) {
3037
- log(pc9.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3230
+ log(pc11.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
3038
3231
  return 1;
3039
3232
  }
3040
3233
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3041
3234
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3042
- const dir = mkdtempSync(join15(tmpdir(), "flare-rollback-"));
3235
+ const dir = mkdtempSync(join16(tmpdir(), "flare-rollback-"));
3043
3236
  try {
3044
- const file = join15(dir, "rollback.sql");
3045
- writeFileSync10(file, script);
3237
+ const file = join16(dir, "rollback.sql");
3238
+ writeFileSync11(file, script);
3046
3239
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3047
3240
  if (result.code !== 0) throw new Error(`Rollback failed:
3048
3241
  ${result.output.trim()}`);
3049
3242
  } finally {
3050
- rmSync(dir, { recursive: true, force: true });
3243
+ rmSync2(dir, { recursive: true, force: true });
3051
3244
  }
3052
- log(pc9.green(`
3245
+ log(pc11.green(`
3053
3246
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3054
3247
  return 0;
3055
3248
  }
3056
3249
 
3057
3250
  // 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";
3251
+ import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync11, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3252
+ import { dirname as dirname10, join as join17 } from "path";
3060
3253
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3061
- import pc10 from "picocolors";
3254
+ import pc12 from "picocolors";
3062
3255
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
3063
3256
  function handWrittenReason(source, header, options2 = {}) {
3064
3257
  const parts = splitMarkers(source);
@@ -3075,10 +3268,10 @@ function handWrittenReason(source, header, options2 = {}) {
3075
3268
  return null;
3076
3269
  }
3077
3270
  function removeEmptyDirs(appRoot, start) {
3078
- let dir = dirname10(join16(appRoot, start));
3079
- const roots = [join16(appRoot, "app", "api"), join16(appRoot, "app", "admin")];
3271
+ let dir = dirname10(join17(appRoot, start));
3272
+ const roots = [join17(appRoot, "app", "api"), join17(appRoot, "app", "admin")];
3080
3273
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3081
- while (inside() && existsSync17(dir) && readdirSync7(dir).length === 0) {
3274
+ while (inside() && existsSync18(dir) && readdirSync7(dir).length === 0) {
3082
3275
  rmdirSync(dir);
3083
3276
  dir = dirname10(dir);
3084
3277
  }
@@ -3107,7 +3300,7 @@ async function rmResource(rawName, options2 = {}) {
3107
3300
  if (entry) {
3108
3301
  owned.add(entry.file);
3109
3302
  for (const file of planFiles(all)) {
3110
- if (existsSync17(join16(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3303
+ if (existsSync18(join17(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
3111
3304
  }
3112
3305
  }
3113
3306
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3116,7 +3309,7 @@ async function rmResource(rawName, options2 = {}) {
3116
3309
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3117
3310
  const findings = [];
3118
3311
  for (const path of owned) {
3119
- const source = readFileSync11(join16(appRoot, path), "utf8");
3312
+ const source = readFileSync11(join17(appRoot, path), "utf8");
3120
3313
  let wrapper;
3121
3314
  if (entry && path === entry.file) {
3122
3315
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3137,34 +3330,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
3137
3330
  }
3138
3331
  const removed = [...owned].sort();
3139
3332
  for (const path of removed) {
3140
- unlinkSync2(join16(appRoot, path));
3333
+ unlinkSync2(join17(appRoot, path));
3141
3334
  removeEmptyDirs(appRoot, path);
3142
- log(`${pc10.red("remove".padEnd(9))} ${path}`);
3335
+ log(`${pc12.red("remove".padEnd(9))} ${path}`);
3143
3336
  }
3144
3337
  logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
3145
3338
  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"))
3339
+ if (table && existsSync18(join17(appRoot, "seeds"))) {
3340
+ const seeds = readdirSync7(join17(appRoot, "seeds")).filter(
3341
+ (file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(join17(appRoot, "seeds", file), "utf8"))
3149
3342
  );
3150
- if (seeds.length) log(pc10.yellow(`
3343
+ if (seeds.length) log(pc12.yellow(`
3151
3344
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
3152
3345
  }
3153
3346
  let migrations = [];
3154
3347
  if (!options2.skipMigration && entry) {
3155
3348
  const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
3156
3349
  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}`));
3350
+ for (const file of migrations) log(`${pc12.green("create".padEnd(9))} ${file}`);
3351
+ for (const repair of result.repairs) log(pc12.dim(`repaired ${repair}`));
3159
3352
  if (migrations.length) log(`
3160
- Next: ${pc10.bold("flare migrate")} to drop the table locally.`);
3353
+ Next: ${pc12.bold("flare migrate")} to drop the table locally.`);
3161
3354
  }
3162
3355
  return { removed, migrations };
3163
3356
  }
3164
3357
 
3165
3358
  // src/commands/role.ts
3166
3359
  import { humanize } from "@flaredev/core";
3167
- import pc11 from "picocolors";
3360
+ import pc13 from "picocolors";
3168
3361
  var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
3169
3362
  function roleInsertSql(name, label) {
3170
3363
  if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
@@ -3209,24 +3402,24 @@ ${result.output.trim()}`);
3209
3402
  }
3210
3403
  const [first] = extractJson(result.output);
3211
3404
  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" : ""}`));
3405
+ log(pc13.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3406
+ log(pc13.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
3214
3407
  return 0;
3215
3408
  }
3216
3409
 
3217
3410
  // src/commands/seed.ts
3218
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "fs";
3411
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync12 } from "fs";
3219
3412
  import { createRequire } from "module";
3220
- import { join as join17 } from "path";
3413
+ import { join as join18 } from "path";
3221
3414
  import { pathToFileURL } from "url";
3222
3415
  import { kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
3223
3416
  import { createJiti as createJiti4 } from "jiti";
3224
- import pc12 from "picocolors";
3417
+ import pc14 from "picocolors";
3225
3418
  var SEEDS_DIR = "seeds";
3226
3419
  var LOCAL_STATE = ".wrangler/state/v3";
3227
3420
  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() : [];
3421
+ const dir = join18(appRoot, SEEDS_DIR);
3422
+ const all = existsSync19(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
3230
3423
  if (!names?.length) return all;
3231
3424
  return names.map((name) => {
3232
3425
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3241,29 +3434,29 @@ async function runSeeds(options2 = {}) {
3241
3434
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3242
3435
  const files = seedFiles(appRoot, options2.names);
3243
3436
  if (files.length === 0) {
3244
- log(`No seeds found. Create one with ${pc12.bold("flare seed:make <name>")}.`);
3437
+ log(`No seeds found. Create one with ${pc14.bold("flare seed:make <name>")}.`);
3245
3438
  return;
3246
3439
  }
3247
- const jiti = createJiti4(join17(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3248
- const appRequire = createRequire(join17(appRoot, "package.json"));
3440
+ const jiti = createJiti4(join18(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3441
+ const appRequire = createRequire(join18(appRoot, "package.json"));
3249
3442
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3250
3443
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3251
- const schema = await jiti.import(join17(appRoot, "db/schema.ts"));
3444
+ const schema = await jiti.import(join18(appRoot, "db/schema.ts"));
3252
3445
  const { env, dispose } = await wrangler.getPlatformProxy({
3253
- configPath: join17(appRoot, "wrangler.jsonc"),
3254
- persist: { path: join17(appRoot, LOCAL_STATE) }
3446
+ configPath: join18(appRoot, "wrangler.jsonc"),
3447
+ persist: { path: join18(appRoot, LOCAL_STATE) }
3255
3448
  });
3256
3449
  try {
3257
3450
  const bindings = env;
3258
3451
  if (!bindings.DB) throw new Error("No DB binding found in wrangler.jsonc.");
3259
3452
  const db = drizzle(bindings.DB, { schema });
3260
3453
  for (const file of files) {
3261
- const mod = await jiti.import(join17(appRoot, SEEDS_DIR, file));
3454
+ const mod = await jiti.import(join18(appRoot, SEEDS_DIR, file));
3262
3455
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3263
- log(`${pc12.cyan("seed")} ${file}`);
3456
+ log(`${pc14.cyan("seed")} ${file}`);
3264
3457
  await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
3265
3458
  }
3266
- log(pc12.green(`
3459
+ log(pc14.green(`
3267
3460
  Ran ${files.length} seed(s) against the local database.`));
3268
3461
  } finally {
3269
3462
  await dispose();
@@ -3332,8 +3525,8 @@ async function makeSeed(rawName, options2 = {}) {
3332
3525
  const name = kebabCase3(rawName);
3333
3526
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3334
3527
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3335
- const path = join17(appRoot, relative2);
3336
- if (existsSync18(path)) throw new Error(`${relative2} already exists.`);
3528
+ const path = join18(appRoot, relative2);
3529
+ if (existsSync19(path)) throw new Error(`${relative2} already exists.`);
3337
3530
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3338
3531
  let resource;
3339
3532
  if (options2.resource) {
@@ -3342,26 +3535,26 @@ async function makeSeed(rawName, options2 = {}) {
3342
3535
  } else {
3343
3536
  resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
3344
3537
  }
3345
- mkdirSync9(join17(appRoot, SEEDS_DIR), { recursive: true });
3346
- writeFileSync11(path, renderSeed(resource));
3347
- log(`${pc12.green("create".padEnd(9))} ${relative2}`);
3538
+ mkdirSync10(join18(appRoot, SEEDS_DIR), { recursive: true });
3539
+ writeFileSync12(path, renderSeed(resource));
3540
+ log(`${pc14.green("create".padEnd(9))} ${relative2}`);
3348
3541
  log(`
3349
- Run it with ${pc12.bold(`flare seed ${name}`)}.`);
3542
+ Run it with ${pc14.bold(`flare seed ${name}`)}.`);
3350
3543
  return relative2;
3351
3544
  }
3352
3545
 
3353
3546
  // src/commands/billing-sync.ts
3354
3547
  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";
3548
+ import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "fs";
3356
3549
  import { tmpdir as tmpdir2 } from "os";
3357
- import { join as join18 } from "path";
3550
+ import { join as join19 } from "path";
3358
3551
  import { pathToFileURL as pathToFileURL2 } from "url";
3359
3552
  import { kebabCase as kebabCase4 } from "@flaredev/core";
3360
- import pc13 from "picocolors";
3553
+ import pc15 from "picocolors";
3361
3554
  function stripeKeys(appRoot) {
3362
3555
  const map = {};
3363
- const path = join18(appRoot, ".dev.vars");
3364
- if (existsSync19(path)) {
3556
+ const path = join19(appRoot, ".dev.vars");
3557
+ if (existsSync20(path)) {
3365
3558
  for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
3366
3559
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
3367
3560
  if (match) map[match[1]] = match[2].trim();
@@ -3445,10 +3638,10 @@ async function syncPlans(options2 = {}) {
3445
3638
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
3446
3639
  const db = readD1Databases(appRoot)[0];
3447
3640
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
3448
- const appRequire = createRequire2(join18(appRoot, "package.json"));
3641
+ const appRequire = createRequire2(join19(appRoot, "package.json"));
3449
3642
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
3450
3643
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
3451
- const appName = JSON.parse(readFileSync12(join18(appRoot, "package.json"), "utf8")).name ?? "app";
3644
+ const appName = JSON.parse(readFileSync12(join19(appRoot, "package.json"), "utf8")).name ?? "app";
3452
3645
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
3453
3646
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
3454
3647
  if (products.length === 0) {
@@ -3469,41 +3662,41 @@ async function syncPlans(options2 = {}) {
3469
3662
  ${existingResult.output.trim()}`);
3470
3663
  }
3471
3664
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
3472
- const dir = mkdtempSync2(join18(tmpdir2(), "flare-sync-"));
3665
+ const dir = mkdtempSync2(join19(tmpdir2(), "flare-sync-"));
3473
3666
  try {
3474
- const file = join18(dir, "sync-plans.sql");
3475
- writeFileSync12(file, renderSyncSql(rows));
3667
+ const file = join19(dir, "sync-plans.sql");
3668
+ writeFileSync13(file, renderSyncSql(rows));
3476
3669
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3477
3670
  if (result.code !== 0) throw new Error(`Could not write plans:
3478
3671
  ${result.output.trim()}`);
3479
3672
  } finally {
3480
- rmSync2(dir, { recursive: true, force: true });
3673
+ rmSync3(dir, { recursive: true, force: true });
3481
3674
  }
3482
3675
  let created = 0;
3483
3676
  for (const row of rows) {
3484
3677
  const isNew = !known.has(row.stripePriceId);
3485
3678
  if (isNew) created++;
3486
3679
  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)}`);
3680
+ log(`${(isNew ? pc15.green("create") : pc15.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc15.dim(price)}`);
3488
3681
  }
3489
- for (const name of skipped) log(pc13.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3682
+ for (const name of skipped) log(pc15.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
3490
3683
  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(`
3684
+ 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})`);
3685
+ log(pc15.green(`
3493
3686
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
3494
3687
  return { created, updated: rows.length - created, skipped };
3495
3688
  }
3496
3689
 
3497
3690
  // src/commands/sync.ts
3498
- import { existsSync as existsSync20 } from "fs";
3499
- import { join as join19 } from "path";
3500
- import pc14 from "picocolors";
3691
+ import { existsSync as existsSync21 } from "fs";
3692
+ import { join as join20 } from "path";
3693
+ import pc16 from "picocolors";
3501
3694
  async function syncTypes(options2 = {}) {
3502
3695
  const log = options2.log ?? ((message) => console.log(message));
3503
3696
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3504
3697
  const all = await loadResources(appRoot);
3505
3698
  const policies = await loadPolicies(appRoot);
3506
- const tracksPolicies = policies.length > 0 || existsSync20(join19(appRoot, "policies/index.ts"));
3699
+ const tracksPolicies = policies.length > 0 || existsSync21(join20(appRoot, "policies/index.ts"));
3507
3700
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
3508
3701
  if (!options2.check) ensureSupportFiles(appRoot, log);
3509
3702
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -3515,42 +3708,42 @@ async function syncTypes(options2 = {}) {
3515
3708
  if (drift.length) {
3516
3709
  log(
3517
3710
  `
3518
- ${pc14.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3711
+ ${pc16.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
3519
3712
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
3520
3713
  );
3521
3714
  }
3522
3715
  if (unmarked.length) {
3523
3716
  log(`
3524
- ${pc14.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3717
+ ${pc16.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
3525
3718
  }
3526
3719
  if (orphans.length) {
3527
3720
  const resources = [...new Set(orphans.map((o) => o.resource))];
3528
3721
  log(`
3529
- ${pc14.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
3722
+ ${pc16.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
3530
3723
  for (const orphan of orphans) log(` ${orphan.path}`);
3531
3724
  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.`
3725
+ `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
3726
  );
3534
3727
  }
3535
3728
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
3536
3729
  if (tablesChanged && !options2.check) {
3537
3730
  log(`
3538
- ${pc14.cyan("Tables changed.")} Create a migration with ${pc14.bold("flare gen migration <name> --from-schema")}, then ${pc14.bold("flare migrate")}.`);
3731
+ ${pc16.cyan("Tables changed.")} Create a migration with ${pc16.bold("flare gen migration <name> --from-schema")}, then ${pc16.bold("flare migrate")}.`);
3539
3732
  }
3540
3733
  const problems = drift.length + unmarked.length + orphans.length;
3541
3734
  if (options2.check) {
3542
3735
  const stale = changed.length;
3543
- if (stale === 0 && problems === 0) log(pc14.green("Generated files are in sync with the resource descriptors."));
3736
+ if (stale === 0 && problems === 0) log(pc16.green("Generated files are in sync with the resource descriptors."));
3544
3737
  else log(`
3545
- ${pc14.red("Out of sync:")} ${stale} file(s) would change. Run ${pc14.bold("flare sync-types")}.`);
3738
+ ${pc16.red("Out of sync:")} ${stale} file(s) would change. Run ${pc16.bold("flare sync-types")}.`);
3546
3739
  return stale + problems > 0 ? 1 : 0;
3547
3740
  }
3548
- if (changed.length === 0 && problems === 0) log(pc14.green("Everything is in sync."));
3741
+ if (changed.length === 0 && problems === 0) log(pc16.green("Everything is in sync."));
3549
3742
  return problems > 0 ? 1 : 0;
3550
3743
  }
3551
3744
 
3552
3745
  // src/commands/user-role.ts
3553
- import pc15 from "picocolors";
3746
+ import pc17 from "picocolors";
3554
3747
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
3555
3748
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
3556
3749
  function userRoleSql(email, role) {
@@ -3564,8 +3757,8 @@ async function setUserRole(email, role, options2 = {}) {
3564
3757
  const sql2 = userRoleSql(email, role);
3565
3758
  const roles = await listRoles(options2);
3566
3759
  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" : ""}`));
3760
+ log(pc17.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3761
+ log(pc17.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
3569
3762
  return 1;
3570
3763
  }
3571
3764
  const db = readD1Databases(appRoot)[0];
@@ -3579,10 +3772,10 @@ ${result.output.trim()}`);
3579
3772
  const [first] = extractJson(result.output);
3580
3773
  const updated = first?.results ?? [];
3581
3774
  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.`));
3775
+ log(pc17.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
3583
3776
  return 1;
3584
3777
  }
3585
- log(pc15.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3778
+ log(pc17.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
3586
3779
  return 0;
3587
3780
  }
3588
3781
 
@@ -3594,7 +3787,7 @@ function createCli() {
3594
3787
  const result = createApp(dir, { pm: options2.pm, install, authProviders: options2.authProviders });
3595
3788
  printNextSteps(result, install);
3596
3789
  });
3597
- cli.command("gen <generator> [name]", "Generate code. Generators: resource, migration, policy, billing, security").option("--fields <fields>", 'resource: fields, e.g. "name:string, email:string!, status:enum(lead,customer)"').option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example('flare gen resource Contact --fields "name:string, email:string!, company:belongsTo(Company)?"').example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
3790
+ cli.command("gen <generator> [name]", "Generate code. Generators: resource, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
3598
3791
  async (generator, name, options2) => {
3599
3792
  if (generator === "billing")
3600
3793
  return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
@@ -3636,6 +3829,18 @@ function createCli() {
3636
3829
  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
3830
  process.exitCode = await syncTypes(options2);
3638
3831
  });
3832
+ 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) => {
3833
+ const local = toLocalUrl(target);
3834
+ const tunnel = await openTunnel(local);
3835
+ console.log(tunnelBanner(tunnel.url, local));
3836
+ process.exitCode = await new Promise((resolve6) => {
3837
+ process.once("SIGINT", () => {
3838
+ tunnel.close();
3839
+ resolve6(0);
3840
+ });
3841
+ tunnel.process.once("exit", (code) => resolve6(code ?? 0));
3842
+ });
3843
+ });
3639
3844
  for (const [name, spec] of Object.entries(DELEGATED_COMMANDS)) {
3640
3845
  cli.command(`${name} [...args]`, spec.description).allowUnknownOptions();
3641
3846
  }
@@ -3659,6 +3864,6 @@ try {
3659
3864
  }
3660
3865
  }
3661
3866
  } catch (error) {
3662
- console.error(pc16.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
3867
+ console.error(pc18.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
3663
3868
  process.exitCode = 1;
3664
3869
  }