@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/README.md +47 -47
- package/dist/index.js +463 -258
- package/package.json +2 -2
- package/templates/app/README.md +7 -4
- package/templates/app/lib/auth.ts +45 -44
- package/templates/app/vite.config.ts +29 -27
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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)}
|
|
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(
|
|
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
|
|
246
|
-
import { dirname as dirname6, join as
|
|
247
|
-
import
|
|
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
|
|
1235
|
-
import { existsSync as
|
|
1236
|
-
import { join as
|
|
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
|
|
1240
|
-
import { existsSync as
|
|
1241
|
-
import { dirname as dirname4, join as
|
|
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
|
|
1246
|
-
import { join as
|
|
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
|
|
1391
|
+
import pc3 from "picocolors";
|
|
1250
1392
|
|
|
1251
1393
|
// src/utils/wrangler.ts
|
|
1252
|
-
import { spawn } from "child_process";
|
|
1253
|
-
import { existsSync as
|
|
1254
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
1280
|
-
if (!
|
|
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 =
|
|
1321
|
-
if (!
|
|
1322
|
-
const jiti = createJiti2(
|
|
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(
|
|
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
|
-
${
|
|
1348
|
-
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) =>
|
|
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 =
|
|
1408
|
-
const optional =
|
|
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
|
|
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(
|
|
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,
|
|
1451
|
-
const pkgDir = findUp(
|
|
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 =
|
|
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?.[
|
|
1458
|
-
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${
|
|
1459
|
-
return
|
|
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 =
|
|
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 && !
|
|
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) =>
|
|
1712
|
+
var listSql = (dir) => existsSync9(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
|
|
1520
1713
|
async function generateSchemaMigration(appRoot, name, options2 = {}) {
|
|
1521
|
-
const dir =
|
|
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 =
|
|
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 =
|
|
1742
|
+
const path = join8(dir, file);
|
|
1550
1743
|
const { sql: sql2, changes } = repairRebuildMigration(readFileSync6(path, "utf8"), snapshotColumns(previous));
|
|
1551
1744
|
if (changes.length) {
|
|
1552
|
-
|
|
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 =
|
|
1561
|
-
if (!prefix || !
|
|
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(
|
|
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
|
|
1568
|
-
import { dirname as dirname5, join as
|
|
1569
|
-
import
|
|
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
|
|
1573
|
-
import { join as
|
|
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 =
|
|
1581
|
-
if (!
|
|
1582
|
-
const jiti = createJiti3(
|
|
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(
|
|
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 =
|
|
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) =>
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 =
|
|
1966
|
-
if (
|
|
1967
|
-
|
|
1968
|
-
copyFileSync(
|
|
1969
|
-
log(`${
|
|
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" ?
|
|
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 =
|
|
1997
|
-
const text =
|
|
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 =
|
|
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 =
|
|
2014
|
-
if (!
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
log(`${
|
|
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(`${
|
|
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(`${
|
|
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 (
|
|
2045
|
-
log(`${
|
|
2237
|
+
if (existsSync12(join11(appRoot, relative2))) {
|
|
2238
|
+
log(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2046
2239
|
continue;
|
|
2047
2240
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
log(`${
|
|
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(
|
|
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(
|
|
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" ?
|
|
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 =
|
|
2062
|
-
if (!
|
|
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(`${
|
|
2260
|
+
log(`${pc6.red("remove".padEnd(9))} ${path} (no longer generated)`);
|
|
2068
2261
|
} else {
|
|
2069
|
-
log(
|
|
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(`${
|
|
2073
|
-
if (appendVars(appRoot, ".dev.vars")) log(`${
|
|
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(`${
|
|
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(`${
|
|
2090
|
-
for (const repair of result.repairs) log(
|
|
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. ${
|
|
2098
|
-
` 3. Create one Product per plan in Stripe with metadata flare_app=${appName}, then ${
|
|
2099
|
-
` 4. ${
|
|
2100
|
-
` 5. ${
|
|
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
|
|
2108
|
-
import { dirname as dirname7, join as
|
|
2109
|
-
import
|
|
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 =
|
|
2637
|
-
if (
|
|
2638
|
-
log(`${
|
|
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
|
-
|
|
2642
|
-
|
|
2643
|
-
log(`${
|
|
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(
|
|
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 =
|
|
2655
|
-
if (path === "lib/security.ts" &&
|
|
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))
|
|
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" ?
|
|
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 =
|
|
2666
|
-
if (
|
|
2667
|
-
else log(
|
|
2668
|
-
const wranglerPath =
|
|
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 (
|
|
2863
|
+
if (existsSync13(wranglerPath)) {
|
|
2671
2864
|
const { text, added } = addSecurityBindings(readFileSync9(wranglerPath, "utf8"));
|
|
2672
2865
|
bindings = added;
|
|
2673
2866
|
if (added.length) {
|
|
2674
|
-
|
|
2675
|
-
for (const line of added) log(`${
|
|
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(
|
|
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(`${
|
|
2688
|
-
for (const repair of result.repairs) log(
|
|
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. ${
|
|
2695
|
-
` 2. ${
|
|
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). ${
|
|
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
|
|
2706
|
-
import { dirname as dirname8, join as
|
|
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
|
|
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(
|
|
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 =
|
|
2728
|
-
const exists =
|
|
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
|
-
|
|
2739
|
-
|
|
2740
|
-
log(`${
|
|
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" ?
|
|
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(
|
|
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(`${
|
|
2778
|
-
for (const repair of result.repairs) log(
|
|
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: ${
|
|
2781
|
-
else if (exists) log(
|
|
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
|
|
2789
|
-
import { basename as basename2, join as
|
|
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
|
|
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(`${
|
|
2808
|
-
for (const repair of repairs) log(
|
|
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 =
|
|
2811
|
-
|
|
3003
|
+
const downDir = join14(appRoot, "migrations", "down");
|
|
3004
|
+
mkdirSync8(downDir, { recursive: true });
|
|
2812
3005
|
for (const file of files) {
|
|
2813
|
-
const down =
|
|
2814
|
-
if (
|
|
2815
|
-
|
|
2816
|
-
log(`${
|
|
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 ${
|
|
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: ${
|
|
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
|
|
2829
|
-
import { dirname as dirname9, join as
|
|
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
|
|
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 =
|
|
2848
|
-
const exists =
|
|
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
|
-
|
|
2860
|
-
|
|
2861
|
-
log(`${
|
|
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" ?
|
|
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
|
-
${
|
|
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
|
|
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
|
|
2885
|
-
import
|
|
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 =
|
|
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 =
|
|
3012
|
-
if (
|
|
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 =
|
|
3021
|
-
if (!
|
|
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(`${
|
|
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} ${
|
|
3034
|
-
for (const line of step.sql.trim().split("\n")) log(
|
|
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(
|
|
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(
|
|
3235
|
+
const dir = mkdtempSync(join16(tmpdir(), "flare-rollback-"));
|
|
3043
3236
|
try {
|
|
3044
|
-
const file =
|
|
3045
|
-
|
|
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
|
-
|
|
3243
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
3051
3244
|
}
|
|
3052
|
-
log(
|
|
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
|
|
3059
|
-
import { dirname as dirname10, join as
|
|
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
|
|
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(
|
|
3079
|
-
const roots = [
|
|
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() &&
|
|
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 (
|
|
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(
|
|
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(
|
|
3333
|
+
unlinkSync2(join17(appRoot, path));
|
|
3141
3334
|
removeEmptyDirs(appRoot, path);
|
|
3142
|
-
log(`${
|
|
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 &&
|
|
3147
|
-
const seeds = readdirSync7(
|
|
3148
|
-
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(
|
|
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(
|
|
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(`${
|
|
3158
|
-
for (const repair of result.repairs) log(
|
|
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: ${
|
|
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
|
|
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(
|
|
3213
|
-
log(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3229
|
-
const all =
|
|
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 ${
|
|
3437
|
+
log(`No seeds found. Create one with ${pc14.bold("flare seed:make <name>")}.`);
|
|
3245
3438
|
return;
|
|
3246
3439
|
}
|
|
3247
|
-
const jiti = createJiti4(
|
|
3248
|
-
const appRequire = createRequire(
|
|
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(
|
|
3444
|
+
const schema = await jiti.import(join18(appRoot, "db/schema.ts"));
|
|
3252
3445
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3253
|
-
configPath:
|
|
3254
|
-
persist: { path:
|
|
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(
|
|
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(`${
|
|
3456
|
+
log(`${pc14.cyan("seed")} ${file}`);
|
|
3264
3457
|
await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
|
|
3265
3458
|
}
|
|
3266
|
-
log(
|
|
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 =
|
|
3336
|
-
if (
|
|
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
|
-
|
|
3346
|
-
|
|
3347
|
-
log(`${
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
3553
|
+
import pc15 from "picocolors";
|
|
3361
3554
|
function stripeKeys(appRoot) {
|
|
3362
3555
|
const map = {};
|
|
3363
|
-
const path =
|
|
3364
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
3665
|
+
const dir = mkdtempSync2(join19(tmpdir2(), "flare-sync-"));
|
|
3473
3666
|
try {
|
|
3474
|
-
const file =
|
|
3475
|
-
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
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(`${
|
|
3492
|
-
log(
|
|
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
|
|
3499
|
-
import { join as
|
|
3500
|
-
import
|
|
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 ||
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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 ${
|
|
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
|
-
${
|
|
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(
|
|
3736
|
+
if (stale === 0 && problems === 0) log(pc16.green("Generated files are in sync with the resource descriptors."));
|
|
3544
3737
|
else log(`
|
|
3545
|
-
${
|
|
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(
|
|
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
|
|
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(
|
|
3568
|
-
log(
|
|
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(
|
|
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(
|
|
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>",
|
|
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(
|
|
3867
|
+
console.error(pc18.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
3663
3868
|
process.exitCode = 1;
|
|
3664
3869
|
}
|