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