@flaredev/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -47
- package/dist/index.js +379 -146
- package/package.json +2 -2
- package/templates/app/app/dashboard/actions.ts +1 -1
- package/templates/app/app/dashboard/error.tsx +37 -0
- package/templates/app/app/dashboard/import-actions.ts +1 -1
- package/templates/app/app/dashboard/loading.tsx +18 -0
- package/templates/app/app/dashboard/not-found.tsx +30 -0
- package/templates/app/app/error.tsx +55 -0
- package/templates/app/app/loading.tsx +22 -0
- package/templates/app/app/not-found.tsx +48 -0
- package/templates/app/components/dashboard/fields/file-cell.tsx +68 -0
- package/templates/app/components/dashboard/record-detail.tsx +4 -0
- package/templates/app/components/dashboard/resource-table.tsx +6 -1
- package/templates/app/components/dashboard/skeletons.tsx +146 -0
- package/templates/app/lib/api.ts +1 -1
- package/templates/app/lib/cache.ts +83 -83
- package/templates/app/lib/dashboard.ts +2 -2
- package/templates/app/lib/resource/drizzle-rows.ts +134 -0
- package/templates/app/lib/resource/handlers.ts +157 -0
- package/templates/app/lib/resource/index.ts +14 -0
- package/templates/app/lib/resource/query.ts +149 -0
- package/templates/app/lib/resource/rows.ts +66 -0
- package/templates/app/lib/resource/store.ts +342 -0
- package/templates/next/app/dashboard/actions.ts +1 -1
- package/templates/next/lib/dashboard.ts +1 -1
- package/templates/next/lib/resource/index.ts +14 -0
- package/templates/next/lib/resource/prisma-rows.ts +169 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import pc25 from "picocolors";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { cac } from "cac";
|
|
6
|
-
import
|
|
6
|
+
import pc24 from "picocolors";
|
|
7
7
|
import * as prompts3 from "@clack/prompts";
|
|
8
8
|
import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
|
|
9
9
|
|
|
@@ -201,6 +201,9 @@ var NEXT_DEV_DEPENDENCIES = {
|
|
|
201
201
|
prisma: "7.10.0",
|
|
202
202
|
// Runs seeds: the generated Prisma client is TypeScript a bundler compiles.
|
|
203
203
|
tsx: "^4.23.13",
|
|
204
|
+
// `flare deploy` and `npm run deploy`. Local rather than global, so the deploy works
|
|
205
|
+
// on a fresh clone and every app pins its own version.
|
|
206
|
+
vercel: "^60.1.3",
|
|
204
207
|
"@types/node": "^22.20.3",
|
|
205
208
|
"@types/react": "^19.3.0",
|
|
206
209
|
"@types/react-dom": "^19.3.0",
|
|
@@ -216,7 +219,9 @@ var CLOUDFLARE_ONLY = [
|
|
|
216
219
|
"migrations",
|
|
217
220
|
"drizzle.config.ts",
|
|
218
221
|
".dev.vars.example",
|
|
219
|
-
"worker-configuration.d.ts"
|
|
222
|
+
"worker-configuration.d.ts",
|
|
223
|
+
// The engine's Drizzle adapter; the Next.js overlay brings prisma-rows.ts instead.
|
|
224
|
+
"lib/resource/drizzle-rows.ts"
|
|
220
225
|
];
|
|
221
226
|
|
|
222
227
|
// src/stack.ts
|
|
@@ -224,6 +229,10 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
|
224
229
|
import { join } from "path";
|
|
225
230
|
var STACKS = ["cloudflare", "next"];
|
|
226
231
|
var isStack = (value) => STACKS.includes(value);
|
|
232
|
+
var STACK_LABELS = {
|
|
233
|
+
cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
|
|
234
|
+
next: "Next.js on Vercel, Neon Postgres and Prisma \u2014 real Postgres, Node runtime"
|
|
235
|
+
};
|
|
227
236
|
function readStack(appRoot) {
|
|
228
237
|
const file = join(appRoot, "package.json");
|
|
229
238
|
if (!existsSync(file)) return "cloudflare";
|
|
@@ -281,6 +290,20 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? "
|
|
|
281
290
|
const name = userAgent.split("/")[0];
|
|
282
291
|
return isPackageManager(name) ? name : "pnpm";
|
|
283
292
|
}
|
|
293
|
+
function isInstalled(packageManager) {
|
|
294
|
+
const probe = [packageManager, "--version"].join(" ");
|
|
295
|
+
const result = process.platform === "win32" ? spawnSync(probe, { stdio: "ignore", shell: true, env: childEnv() }) : spawnSync(packageManager, ["--version"], { stdio: "ignore", env: childEnv() });
|
|
296
|
+
return result.status === 0;
|
|
297
|
+
}
|
|
298
|
+
function choosePackageManager(explicit) {
|
|
299
|
+
if (explicit !== void 0) {
|
|
300
|
+
if (!isPackageManager(explicit)) throw new Error(`Unknown package manager "${explicit}". Use pnpm, npm, yarn, or bun.`);
|
|
301
|
+
return { packageManager: explicit, preferred: false };
|
|
302
|
+
}
|
|
303
|
+
const detected = detectPackageManager();
|
|
304
|
+
if (detected !== "pnpm" && isInstalled("pnpm")) return { packageManager: "pnpm", preferred: true };
|
|
305
|
+
return { packageManager: detected, preferred: false };
|
|
306
|
+
}
|
|
284
307
|
function run(command, args, cwd) {
|
|
285
308
|
const env = childEnv();
|
|
286
309
|
const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true, env }) : spawnSync(command, args, { cwd, stdio: "inherit", env });
|
|
@@ -321,10 +344,7 @@ function createApp(target, options2 = {}) {
|
|
|
321
344
|
if (existsSync3(dir) && readdirSync2(dir).length > 0) {
|
|
322
345
|
throw new Error(`Directory ${dir} already exists and is not empty.`);
|
|
323
346
|
}
|
|
324
|
-
|
|
325
|
-
throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
|
|
326
|
-
}
|
|
327
|
-
const packageManager = options2.pm ?? detectPackageManager();
|
|
347
|
+
const { packageManager, preferred: prefersPnpm } = choosePackageManager(options2.pm);
|
|
328
348
|
if (options2.stack !== void 0 && !isStack(options2.stack)) {
|
|
329
349
|
throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
|
|
330
350
|
}
|
|
@@ -398,6 +418,7 @@ ${devVarsEntries(authProviders)}`
|
|
|
398
418
|
if (pinned) copyLockfile(packageManager, dir);
|
|
399
419
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
400
420
|
log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
|
|
421
|
+
if (prefersPnpm) log2(pc.dim(`Installing with pnpm \u2014 far quicker for an app this size. Use ${pc.bold("--pm npm")} to change that.`));
|
|
401
422
|
return { dir, name, packageManager, inWorkspace };
|
|
402
423
|
}
|
|
403
424
|
function copyLockfile(packageManager, dir) {
|
|
@@ -1794,12 +1815,14 @@ function resolveBin(appRoot, pkgName, binName2) {
|
|
|
1794
1815
|
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
|
|
1795
1816
|
return join8(dirname4(manifestPath), relativeBin);
|
|
1796
1817
|
}
|
|
1818
|
+
var NEXT_COMMANDS = {
|
|
1819
|
+
dev: { pkg: "next", bin: "next", args: ["dev"] },
|
|
1820
|
+
build: { pkg: "next", bin: "next", args: ["build"] },
|
|
1821
|
+
start: { pkg: "next", bin: "next", args: ["start"] },
|
|
1822
|
+
deploy: { pkg: "vercel", bin: "vercel", args: ["deploy", "--prod"] }
|
|
1823
|
+
};
|
|
1797
1824
|
function delegatedArgv(appRoot, command, forwarded) {
|
|
1798
|
-
|
|
1799
|
-
const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
|
|
1800
|
-
throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
|
|
1801
|
-
}
|
|
1802
|
-
const spec = DELEGATED_COMMANDS[command];
|
|
1825
|
+
const spec = readStack(appRoot) === "next" ? NEXT_COMMANDS[command] : DELEGATED_COMMANDS[command];
|
|
1803
1826
|
return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
|
|
1804
1827
|
}
|
|
1805
1828
|
function runNode(argv, cwd) {
|
|
@@ -1854,15 +1877,22 @@ function portArg(args) {
|
|
|
1854
1877
|
}
|
|
1855
1878
|
async function runDelegated(command, forwarded, cwd = process.cwd()) {
|
|
1856
1879
|
const appRoot = findAppRoot(cwd);
|
|
1880
|
+
const next = readStack(appRoot) === "next";
|
|
1857
1881
|
const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
|
|
1858
1882
|
if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
|
|
1859
1883
|
const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
|
|
1860
|
-
if (command === "
|
|
1884
|
+
if (next && command === "build" && !wantsHelp) {
|
|
1885
|
+
const code = await runNode([resolveBin(appRoot, "prisma", "prisma"), "generate"], appRoot);
|
|
1886
|
+
if (code !== 0) return code;
|
|
1887
|
+
}
|
|
1888
|
+
const built = next ? ".next" : "dist/server/wrangler.json";
|
|
1889
|
+
if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, built))) {
|
|
1861
1890
|
console.log("No production build found; running `flare build` first.\n");
|
|
1862
|
-
const code = await
|
|
1891
|
+
const code = await runDelegated("build", [], appRoot);
|
|
1863
1892
|
if (code !== 0) return code;
|
|
1864
1893
|
}
|
|
1865
1894
|
if (command === "deploy") {
|
|
1895
|
+
if (next) return runNode(delegatedArgv(appRoot, "deploy", forwarded), appRoot);
|
|
1866
1896
|
return runDeploy(forwarded, {
|
|
1867
1897
|
appRoot,
|
|
1868
1898
|
wranglerBin: resolveBin(appRoot, "wrangler", "wrangler"),
|
|
@@ -1956,7 +1986,7 @@ function previousSnapshot(dir, migrationFile) {
|
|
|
1956
1986
|
|
|
1957
1987
|
// src/generator/plan.ts
|
|
1958
1988
|
import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
1959
|
-
import { dirname as dirname5, join as join11 } from "path";
|
|
1989
|
+
import { dirname as dirname5, join as join11, sep } from "path";
|
|
1960
1990
|
import pc5 from "picocolors";
|
|
1961
1991
|
|
|
1962
1992
|
// src/generator/policy.ts
|
|
@@ -2281,14 +2311,19 @@ ${lines.join("\n")}
|
|
|
2281
2311
|
var handlerImports = (entry, stack = "cloudflare") => {
|
|
2282
2312
|
const local = resourceLocal(entry.stem);
|
|
2283
2313
|
const source = stack === "next" ? {
|
|
2284
|
-
imports: [`import { prismaRows } from "
|
|
2314
|
+
imports: [`import { createResourceHandlers, prismaRows } from "@/lib/resource";`, `import { prisma } from "@/lib/db";`],
|
|
2285
2315
|
wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
|
|
2286
2316
|
} : {
|
|
2287
|
-
imports: [
|
|
2288
|
-
|
|
2317
|
+
imports: [
|
|
2318
|
+
`import { createResourceHandlers, drizzleRows } from "@/lib/resource";`,
|
|
2319
|
+
`import { getDb } from "@/db";`,
|
|
2320
|
+
`import { ${tableExport(entry.resource)} } from "@/db/schema";`
|
|
2321
|
+
],
|
|
2322
|
+
// Named rather than implied: the adapter is the one thing about this route
|
|
2323
|
+
// that differs between stacks, so it is written out.
|
|
2324
|
+
wiring: [` rows: drizzleRows(${tableExport(entry.resource)}, getDb),`]
|
|
2289
2325
|
};
|
|
2290
2326
|
return [
|
|
2291
|
-
`import { createResourceHandlers } from "@flaredev/core/server";`,
|
|
2292
2327
|
...source.imports,
|
|
2293
2328
|
`import { authorize, currentUser } from "@/lib/api";`,
|
|
2294
2329
|
`import { revalidateResource } from "@/lib/cache";`,
|
|
@@ -2322,6 +2357,49 @@ function renderItemRoute(entry, stack = "cloudflare") {
|
|
|
2322
2357
|
var adminImports = (entry) => [
|
|
2323
2358
|
`import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`
|
|
2324
2359
|
];
|
|
2360
|
+
function renderAdminListLoading(entry) {
|
|
2361
|
+
const columns = Math.min(6, storedFields2(entry.resource).length + 1);
|
|
2362
|
+
return [
|
|
2363
|
+
`import { ResourceListSkeleton } from "@/components/dashboard/skeletons";`,
|
|
2364
|
+
"",
|
|
2365
|
+
`export default function Loading() {`,
|
|
2366
|
+
` return <ResourceListSkeleton columns={${columns}} />;`,
|
|
2367
|
+
"}",
|
|
2368
|
+
""
|
|
2369
|
+
].join("\n");
|
|
2370
|
+
}
|
|
2371
|
+
function renderAdminFormLoading(entry) {
|
|
2372
|
+
const fields = Math.min(8, Math.max(1, storedFields2(entry.resource).length));
|
|
2373
|
+
return [
|
|
2374
|
+
`import { FormSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
|
|
2375
|
+
"",
|
|
2376
|
+
`export default function Loading() {`,
|
|
2377
|
+
" return (",
|
|
2378
|
+
" <>",
|
|
2379
|
+
" <PageHeaderSkeleton actions={0} />",
|
|
2380
|
+
` <FormSkeleton fields={${fields}} />`,
|
|
2381
|
+
" </>",
|
|
2382
|
+
" );",
|
|
2383
|
+
"}",
|
|
2384
|
+
""
|
|
2385
|
+
].join("\n");
|
|
2386
|
+
}
|
|
2387
|
+
function renderAdminDetailLoading(entry) {
|
|
2388
|
+
const rows = Math.min(10, Math.max(1, storedFields2(entry.resource).length));
|
|
2389
|
+
return [
|
|
2390
|
+
`import { DetailSkeleton, PageHeaderSkeleton } from "@/components/dashboard/skeletons";`,
|
|
2391
|
+
"",
|
|
2392
|
+
`export default function Loading() {`,
|
|
2393
|
+
" return (",
|
|
2394
|
+
" <>",
|
|
2395
|
+
" <PageHeaderSkeleton />",
|
|
2396
|
+
` <DetailSkeleton rows={${rows}} />`,
|
|
2397
|
+
" </>",
|
|
2398
|
+
" );",
|
|
2399
|
+
"}",
|
|
2400
|
+
""
|
|
2401
|
+
].join("\n");
|
|
2402
|
+
}
|
|
2325
2403
|
function renderAdminListPage(entry) {
|
|
2326
2404
|
const local = resourceLocal(entry.stem);
|
|
2327
2405
|
return [
|
|
@@ -2464,7 +2542,11 @@ function resourceFiles(entry, all, stack = "cloudflare") {
|
|
|
2464
2542
|
{ path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
|
|
2465
2543
|
{ path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
|
|
2466
2544
|
{ path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
|
|
2467
|
-
{ path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
|
|
2545
|
+
{ path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) },
|
|
2546
|
+
{ path: `app/dashboard/${resource.slug}/loading.tsx`, content: renderAdminListLoading(entry) },
|
|
2547
|
+
{ path: `app/dashboard/${resource.slug}/new/loading.tsx`, content: renderAdminFormLoading(entry) },
|
|
2548
|
+
{ path: `app/dashboard/${resource.slug}/[id]/loading.tsx`, content: renderAdminDetailLoading(entry) },
|
|
2549
|
+
{ path: `app/dashboard/${resource.slug}/[id]/edit/loading.tsx`, content: renderAdminFormLoading(entry) }
|
|
2468
2550
|
];
|
|
2469
2551
|
}
|
|
2470
2552
|
|
|
@@ -2546,7 +2628,9 @@ function findOrphans(appRoot, all) {
|
|
|
2546
2628
|
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2547
2629
|
}
|
|
2548
2630
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2549
|
-
|
|
2631
|
+
const pages = [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`];
|
|
2632
|
+
const skeletons = [`${dir}/loading.tsx`, `${dir}/new/loading.tsx`, `${dir}/[id]/loading.tsx`, `${dir}/[id]/edit/loading.tsx`];
|
|
2633
|
+
for (const file of [...pages, ...skeletons]) {
|
|
2550
2634
|
if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2551
2635
|
}
|
|
2552
2636
|
}
|
|
@@ -2558,11 +2642,19 @@ function findOrphans(appRoot, all) {
|
|
|
2558
2642
|
return orphans;
|
|
2559
2643
|
}
|
|
2560
2644
|
function ensureSupportFiles(appRoot, log2) {
|
|
2561
|
-
|
|
2645
|
+
const stack = readStack(appRoot);
|
|
2646
|
+
const engine = ["rows.ts", "query.ts", "store.ts", "handlers.ts", "index.ts", stack === "next" ? "prisma-rows.ts" : "drizzle-rows.ts"].map(
|
|
2647
|
+
(name) => `lib/resource/${name}`
|
|
2648
|
+
);
|
|
2649
|
+
for (const path of ["lib/api.ts", "lib/cache.ts", ...engine]) {
|
|
2562
2650
|
const target = join11(appRoot, path);
|
|
2563
2651
|
if (existsSync12(target)) continue;
|
|
2652
|
+
const source = [join11(templatesDir, "next", path), join11(templatesDir, "app", path)].find(
|
|
2653
|
+
(candidate) => (stack === "next" || !candidate.includes(`${sep}next${sep}`)) && existsSync12(candidate)
|
|
2654
|
+
);
|
|
2655
|
+
if (!source) continue;
|
|
2564
2656
|
mkdirSync4(dirname5(target), { recursive: true });
|
|
2565
|
-
copyFileSync2(
|
|
2657
|
+
copyFileSync2(source, target);
|
|
2566
2658
|
log2(`${pc5.green("create".padEnd(9))} ${path}`);
|
|
2567
2659
|
}
|
|
2568
2660
|
}
|
|
@@ -2606,30 +2698,30 @@ function addStripeDependency(appRoot) {
|
|
|
2606
2698
|
return true;
|
|
2607
2699
|
}
|
|
2608
2700
|
function writeBillingResource(appRoot, name, force, log2) {
|
|
2609
|
-
const
|
|
2610
|
-
const path = join12(appRoot,
|
|
2701
|
+
const relative3 = descriptorPath(name);
|
|
2702
|
+
const path = join12(appRoot, relative3);
|
|
2611
2703
|
if (!existsSync13(path)) {
|
|
2612
2704
|
mkdirSync5(dirname6(path), { recursive: true });
|
|
2613
2705
|
writeFileSync7(path, renderBillingDescriptor(name));
|
|
2614
|
-
log2(`${pc6.green("create".padEnd(9))} ${
|
|
2706
|
+
log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
|
|
2615
2707
|
return;
|
|
2616
2708
|
}
|
|
2617
2709
|
const parts = splitMarkers(readFileSync9(path, "utf8"));
|
|
2618
|
-
if (!parts) throw new Error(`${
|
|
2710
|
+
if (!parts) throw new Error(`${relative3} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
|
|
2619
2711
|
const merged = mergeBillingFields(name, parts.block);
|
|
2620
2712
|
if (!merged) {
|
|
2621
|
-
log2(`${pc6.dim("identical".padEnd(9))} ${
|
|
2713
|
+
log2(`${pc6.dim("identical".padEnd(9))} ${relative3} (has every billing field)`);
|
|
2622
2714
|
return;
|
|
2623
2715
|
}
|
|
2624
2716
|
try {
|
|
2625
2717
|
writeGenerated(path, merged, { force });
|
|
2626
2718
|
} catch (error) {
|
|
2627
2719
|
if (error instanceof DriftError) {
|
|
2628
|
-
throw new Error(`${
|
|
2720
|
+
throw new Error(`${relative3}: its fields block was edited by hand, so the billing fields weren't merged. Pass --force, or add them yourself: ${BILLING_RESOURCES[name]}`);
|
|
2629
2721
|
}
|
|
2630
2722
|
throw error;
|
|
2631
2723
|
}
|
|
2632
|
-
log2(`${pc6.yellow("update".padEnd(9))} ${
|
|
2724
|
+
log2(`${pc6.yellow("update".padEnd(9))} ${relative3} (added billing fields, kept yours)`);
|
|
2633
2725
|
}
|
|
2634
2726
|
async function genBilling(options2 = {}) {
|
|
2635
2727
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
@@ -2637,14 +2729,14 @@ async function genBilling(options2 = {}) {
|
|
|
2637
2729
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
2638
2730
|
for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
|
|
2639
2731
|
for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
|
|
2640
|
-
const
|
|
2641
|
-
if (existsSync13(join12(appRoot,
|
|
2642
|
-
log2(`${pc6.dim("kept".padEnd(9))} ${
|
|
2732
|
+
const relative3 = policyPath(name);
|
|
2733
|
+
if (existsSync13(join12(appRoot, relative3))) {
|
|
2734
|
+
log2(`${pc6.dim("kept".padEnd(9))} ${relative3} (already exists)`);
|
|
2643
2735
|
continue;
|
|
2644
2736
|
}
|
|
2645
2737
|
mkdirSync5(join12(appRoot, "policies"), { recursive: true });
|
|
2646
|
-
writeFileSync7(join12(appRoot,
|
|
2647
|
-
log2(`${pc6.green("create".padEnd(9))} ${
|
|
2738
|
+
writeFileSync7(join12(appRoot, relative3), renderPolicy(name, roles));
|
|
2739
|
+
log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
|
|
2648
2740
|
}
|
|
2649
2741
|
const files = [];
|
|
2650
2742
|
const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
@@ -3229,15 +3321,15 @@ function securityFiles(appName) {
|
|
|
3229
3321
|
];
|
|
3230
3322
|
}
|
|
3231
3323
|
var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
|
|
3232
|
-
function writeOnce(appRoot,
|
|
3233
|
-
const path = join13(appRoot,
|
|
3324
|
+
function writeOnce(appRoot, relative3, content, log2) {
|
|
3325
|
+
const path = join13(appRoot, relative3);
|
|
3234
3326
|
if (existsSync14(path)) {
|
|
3235
|
-
log2(`${pc7.dim("kept".padEnd(9))} ${
|
|
3327
|
+
log2(`${pc7.dim("kept".padEnd(9))} ${relative3} (already exists)`);
|
|
3236
3328
|
return;
|
|
3237
3329
|
}
|
|
3238
3330
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
3239
3331
|
writeFileSync8(path, content);
|
|
3240
|
-
log2(`${pc7.green("create".padEnd(9))} ${
|
|
3332
|
+
log2(`${pc7.green("create".padEnd(9))} ${relative3}`);
|
|
3241
3333
|
}
|
|
3242
3334
|
async function genSecurity(options2 = {}) {
|
|
3243
3335
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
@@ -3351,6 +3443,15 @@ function stopIfCancelled(value) {
|
|
|
3351
3443
|
async function askCreateQuestions(given, appName) {
|
|
3352
3444
|
prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
|
|
3353
3445
|
const answers = { ...given };
|
|
3446
|
+
if (given.stack === void 0) {
|
|
3447
|
+
answers.stack = stopIfCancelled(
|
|
3448
|
+
await prompts2.select({
|
|
3449
|
+
message: "Where will it run?",
|
|
3450
|
+
initialValue: "cloudflare",
|
|
3451
|
+
options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
|
|
3452
|
+
})
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3354
3455
|
if (given.theme === void 0) {
|
|
3355
3456
|
answers.theme = stopIfCancelled(
|
|
3356
3457
|
await prompts2.select({
|
|
@@ -3500,16 +3601,16 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3500
3601
|
const action = options2.action ?? (method === "GET" ? "read" : "update");
|
|
3501
3602
|
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3502
3603
|
const { resource, stem } = entry;
|
|
3503
|
-
const
|
|
3504
|
-
const path = join16(appRoot,
|
|
3505
|
-
if (existsSync17(path)) throw new Error(`${
|
|
3604
|
+
const relative3 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
|
|
3605
|
+
const path = join16(appRoot, relative3);
|
|
3606
|
+
if (existsSync17(path)) throw new Error(`${relative3} already exists.`);
|
|
3506
3607
|
mkdirSync8(join16(path, ".."), { recursive: true });
|
|
3507
3608
|
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3508
3609
|
writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3509
|
-
log2(`${pc11.green("create".padEnd(9))} ${
|
|
3610
|
+
log2(`${pc11.green("create".padEnd(9))} ${relative3}`);
|
|
3510
3611
|
log2(`
|
|
3511
3612
|
It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
|
|
3512
|
-
return
|
|
3613
|
+
return relative3;
|
|
3513
3614
|
}
|
|
3514
3615
|
function renderEndpoint({ resource, stem, method, action, record, route }) {
|
|
3515
3616
|
const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
|
|
@@ -3599,8 +3700,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3599
3700
|
const known = resources.map(({ resource }) => resource.name).join(", ") || "none";
|
|
3600
3701
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3601
3702
|
}
|
|
3602
|
-
const
|
|
3603
|
-
const path = join18(appRoot,
|
|
3703
|
+
const relative3 = policyPath(name);
|
|
3704
|
+
const path = join18(appRoot, relative3);
|
|
3604
3705
|
const exists = existsSync19(path);
|
|
3605
3706
|
let roles;
|
|
3606
3707
|
let deleteRoles;
|
|
@@ -3614,14 +3715,14 @@ async function genPolicy(rawName, options2) {
|
|
|
3614
3715
|
if (!exists) {
|
|
3615
3716
|
mkdirSync10(dirname9(path), { recursive: true });
|
|
3616
3717
|
writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3617
|
-
log2(`${pc13.green("create".padEnd(9))} ${
|
|
3718
|
+
log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
|
|
3618
3719
|
} else {
|
|
3619
3720
|
try {
|
|
3620
3721
|
const outcome = writeGenerated(path, block, { force: options2.force });
|
|
3621
|
-
log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${
|
|
3722
|
+
log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative3}`);
|
|
3622
3723
|
} catch (error) {
|
|
3623
3724
|
if (error instanceof DriftError) {
|
|
3624
|
-
throw new Error(`The roles in ${
|
|
3725
|
+
throw new Error(`The roles in ${relative3} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
|
|
3625
3726
|
}
|
|
3626
3727
|
throw error;
|
|
3627
3728
|
}
|
|
@@ -3631,7 +3732,7 @@ async function genPolicy(rawName, options2) {
|
|
|
3631
3732
|
logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
|
|
3632
3733
|
log2(`
|
|
3633
3734
|
${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3634
|
-
return { name, path:
|
|
3735
|
+
return { name, path: relative3 };
|
|
3635
3736
|
}
|
|
3636
3737
|
|
|
3637
3738
|
// src/commands/migrate.ts
|
|
@@ -3980,12 +4081,118 @@ ${result.output.trim()}`);
|
|
|
3980
4081
|
return 0;
|
|
3981
4082
|
}
|
|
3982
4083
|
|
|
4084
|
+
// src/commands/eject.ts
|
|
4085
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync15 } from "fs";
|
|
4086
|
+
import { dirname as dirname11, join as join21, relative as relative2 } from "path";
|
|
4087
|
+
import pc17 from "picocolors";
|
|
4088
|
+
var TRACKED = ["lib/resource"];
|
|
4089
|
+
var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only) => path === only || path.startsWith(`${only}/`));
|
|
4090
|
+
function upstreamFile(path, stack) {
|
|
4091
|
+
if (prunedOn(path, stack)) return void 0;
|
|
4092
|
+
const candidates = stack === "next" ? [join21(templatesDir, "next", path), join21(templatesDir, "app", path)] : [join21(templatesDir, "app", path)];
|
|
4093
|
+
return candidates.find((candidate) => existsSync22(candidate));
|
|
4094
|
+
}
|
|
4095
|
+
function trackedPaths(appRoot, stack) {
|
|
4096
|
+
const paths = /* @__PURE__ */ new Set();
|
|
4097
|
+
const walk = (base, dir) => {
|
|
4098
|
+
if (!existsSync22(join21(base, dir))) return;
|
|
4099
|
+
for (const entry of readdirSync8(join21(base, dir), { withFileTypes: true })) {
|
|
4100
|
+
const next = `${dir}/${entry.name}`;
|
|
4101
|
+
if (entry.isDirectory()) walk(base, next);
|
|
4102
|
+
else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
for (const root of TRACKED) {
|
|
4106
|
+
walk(appRoot, root);
|
|
4107
|
+
walk(join21(templatesDir, "app"), root);
|
|
4108
|
+
if (stack === "next") walk(join21(templatesDir, "next"), root);
|
|
4109
|
+
}
|
|
4110
|
+
return [...paths].filter((path) => upstreamFile(path, stack) !== void 0).sort();
|
|
4111
|
+
}
|
|
4112
|
+
function compareTracked(appRoot, stack, filter) {
|
|
4113
|
+
return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
|
|
4114
|
+
const source = upstreamFile(path, stack);
|
|
4115
|
+
const theirs = readFileSync14(source, "utf8");
|
|
4116
|
+
const target = join21(appRoot, path);
|
|
4117
|
+
if (!existsSync22(target)) return { path, state: "missing", theirs };
|
|
4118
|
+
const mine = readFileSync14(target, "utf8");
|
|
4119
|
+
return { path, state: mine === theirs ? "same" : "changed", mine, theirs };
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
function summarise(mine, theirs) {
|
|
4123
|
+
const a = mine.split("\n");
|
|
4124
|
+
const b = theirs.split("\n");
|
|
4125
|
+
const lines = [];
|
|
4126
|
+
const max = Math.max(a.length, b.length);
|
|
4127
|
+
let shown = 0;
|
|
4128
|
+
for (let index = 0; index < max && shown < 12; index += 1) {
|
|
4129
|
+
if (a[index] === b[index]) continue;
|
|
4130
|
+
if (a[index] !== void 0) lines.push(pc17.red(` - ${a[index].slice(0, 100)}`));
|
|
4131
|
+
if (b[index] !== void 0) lines.push(pc17.green(` + ${b[index].slice(0, 100)}`));
|
|
4132
|
+
shown += 1;
|
|
4133
|
+
}
|
|
4134
|
+
return lines;
|
|
4135
|
+
}
|
|
4136
|
+
function diffTracked(options2 = {}) {
|
|
4137
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4138
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4139
|
+
const stack = readStack(appRoot);
|
|
4140
|
+
const comparisons = compareTracked(appRoot, stack, options2.filter);
|
|
4141
|
+
const changed = comparisons.filter((entry) => entry.state !== "same");
|
|
4142
|
+
if (changed.length === 0) {
|
|
4143
|
+
log2(pc17.green(`Up to date: ${comparisons.length} file(s) match this version of Flare.`));
|
|
4144
|
+
return comparisons;
|
|
4145
|
+
}
|
|
4146
|
+
log2(pc17.bold(`${changed.length} of ${comparisons.length} file(s) differ from this version of Flare:
|
|
4147
|
+
`));
|
|
4148
|
+
for (const entry of changed) {
|
|
4149
|
+
if (entry.state === "missing") {
|
|
4150
|
+
log2(`${pc17.yellow("missing")} ${entry.path} ${pc17.dim("(Flare ships this; your app doesn't have it)")}`);
|
|
4151
|
+
continue;
|
|
4152
|
+
}
|
|
4153
|
+
log2(`${pc17.cyan("changed")} ${entry.path}`);
|
|
4154
|
+
for (const line of summarise(entry.mine, entry.theirs)) log2(line);
|
|
4155
|
+
log2("");
|
|
4156
|
+
}
|
|
4157
|
+
log2(pc17.dim(`Yours is "-", Flare's is "+". Apply with ${pc17.bold("flare update")}, or keep yours and do nothing.`));
|
|
4158
|
+
return comparisons;
|
|
4159
|
+
}
|
|
4160
|
+
function updateTracked(options2 = {}) {
|
|
4161
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4162
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4163
|
+
const stack = readStack(appRoot);
|
|
4164
|
+
const comparisons = compareTracked(appRoot, stack, options2.filter);
|
|
4165
|
+
const changed = comparisons.filter((entry) => entry.state !== "same");
|
|
4166
|
+
if (changed.length === 0) {
|
|
4167
|
+
log2(pc17.green("Nothing to update."));
|
|
4168
|
+
return 0;
|
|
4169
|
+
}
|
|
4170
|
+
if (!options2.yes) {
|
|
4171
|
+
log2(pc17.bold(`${changed.length} file(s) would be overwritten with Flare's version:
|
|
4172
|
+
`));
|
|
4173
|
+
for (const entry of changed) log2(` ${entry.path}`);
|
|
4174
|
+
log2(pc17.yellow(`
|
|
4175
|
+
This discards your edits to those files. Re-run with ${pc17.bold("--yes")} to proceed.`));
|
|
4176
|
+
log2(pc17.dim(`See what would change first with ${pc17.bold("flare diff")}.`));
|
|
4177
|
+
return 1;
|
|
4178
|
+
}
|
|
4179
|
+
for (const entry of changed) {
|
|
4180
|
+
const target = join21(appRoot, entry.path);
|
|
4181
|
+
mkdirSync11(dirname11(target), { recursive: true });
|
|
4182
|
+
writeFileSync15(target, entry.theirs);
|
|
4183
|
+
log2(`${pc17.green("update")} ${relative2(process.cwd(), target) || entry.path}`);
|
|
4184
|
+
}
|
|
4185
|
+
log2(pc17.green(`
|
|
4186
|
+
Updated ${changed.length} file(s).`));
|
|
4187
|
+
return 0;
|
|
4188
|
+
}
|
|
4189
|
+
|
|
3983
4190
|
// src/commands/seed.ts
|
|
3984
|
-
import { existsSync as
|
|
3985
|
-
import { join as
|
|
4191
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
4192
|
+
import { join as join23 } from "path";
|
|
3986
4193
|
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
3987
4194
|
import { createJiti as createJiti4 } from "jiti";
|
|
3988
|
-
import
|
|
4195
|
+
import pc18 from "picocolors";
|
|
3989
4196
|
|
|
3990
4197
|
// src/seed/bulk.ts
|
|
3991
4198
|
var MAX_STATEMENT_BYTES = 8e4;
|
|
@@ -4079,17 +4286,20 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
4079
4286
|
}
|
|
4080
4287
|
|
|
4081
4288
|
// src/seed/local-d1.ts
|
|
4289
|
+
import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
|
|
4082
4290
|
import { createRequire } from "module";
|
|
4083
|
-
import { join as
|
|
4291
|
+
import { join as join22 } from "path";
|
|
4084
4292
|
import { pathToFileURL } from "url";
|
|
4293
|
+
import { parse as parseJsonc3 } from "jsonc-parser";
|
|
4085
4294
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
4086
4295
|
async function openLocalD1(appRoot, binding) {
|
|
4087
4296
|
process.env.WRANGLER_LOG ??= "error";
|
|
4088
|
-
const appRequire = createRequire(
|
|
4297
|
+
const appRequire = createRequire(join22(appRoot, "package.json"));
|
|
4089
4298
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
4299
|
+
const configPath = seedConfig(appRoot);
|
|
4090
4300
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
4091
|
-
configPath
|
|
4092
|
-
persist: { path:
|
|
4301
|
+
configPath,
|
|
4302
|
+
persist: { path: join22(appRoot, LOCAL_STATE) }
|
|
4093
4303
|
});
|
|
4094
4304
|
const bindings = env;
|
|
4095
4305
|
const name = binding ?? "DB";
|
|
@@ -4098,14 +4308,30 @@ async function openLocalD1(appRoot, binding) {
|
|
|
4098
4308
|
const available = Object.keys(bindings).filter((key) => typeof bindings[key]?.prepare === "function");
|
|
4099
4309
|
throw new Error(`No "${name}" database binding in wrangler.jsonc.${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
|
|
4100
4310
|
}
|
|
4101
|
-
return {
|
|
4311
|
+
return {
|
|
4312
|
+
db,
|
|
4313
|
+
env: bindings,
|
|
4314
|
+
dispose: async () => {
|
|
4315
|
+
await dispose();
|
|
4316
|
+
rmSync4(configPath, { force: true });
|
|
4317
|
+
}
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
function seedConfig(appRoot) {
|
|
4321
|
+
const source = join22(appRoot, "wrangler.jsonc");
|
|
4322
|
+
const config = parseJsonc3(readFileSync15(source, "utf8")) ?? {};
|
|
4323
|
+
delete config.durable_objects;
|
|
4324
|
+
delete config.migrations;
|
|
4325
|
+
const path = join22(appRoot, ".flare-seed.wrangler.json");
|
|
4326
|
+
writeFileSync16(path, JSON.stringify(config, null, 2));
|
|
4327
|
+
return path;
|
|
4102
4328
|
}
|
|
4103
4329
|
|
|
4104
4330
|
// src/commands/seed.ts
|
|
4105
4331
|
var SEEDS_DIR = "seeds";
|
|
4106
4332
|
function seedFiles(appRoot, names) {
|
|
4107
|
-
const dir =
|
|
4108
|
-
const all =
|
|
4333
|
+
const dir = join23(appRoot, SEEDS_DIR);
|
|
4334
|
+
const all = existsSync23(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
4109
4335
|
if (!names?.length) return all;
|
|
4110
4336
|
return names.map((name) => {
|
|
4111
4337
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -4120,28 +4346,28 @@ async function runSeeds(options2 = {}) {
|
|
|
4120
4346
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4121
4347
|
const files = seedFiles(appRoot, options2.names);
|
|
4122
4348
|
if (files.length === 0) {
|
|
4123
|
-
log2(`No seeds found. Create one with ${
|
|
4349
|
+
log2(`No seeds found. Create one with ${pc18.bold("flare seed:make <name>")}.`);
|
|
4124
4350
|
return;
|
|
4125
4351
|
}
|
|
4126
4352
|
if (readStack(appRoot) === "next") {
|
|
4127
4353
|
await runPrismaSeeds(appRoot, files, log2);
|
|
4128
4354
|
return;
|
|
4129
4355
|
}
|
|
4130
|
-
const jiti = createJiti4(
|
|
4356
|
+
const jiti = createJiti4(join23(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
4131
4357
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
4132
4358
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
4133
|
-
const schema = await jiti.import(
|
|
4359
|
+
const schema = await jiti.import(join23(appRoot, "db/schema.ts"));
|
|
4134
4360
|
const local = await openLocalD1(appRoot);
|
|
4135
4361
|
try {
|
|
4136
4362
|
const db = drizzle(local.db, { schema });
|
|
4137
4363
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
4138
4364
|
for (const file of files) {
|
|
4139
|
-
const mod = await jiti.import(
|
|
4365
|
+
const mod = await jiti.import(join23(appRoot, SEEDS_DIR, file));
|
|
4140
4366
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
4141
|
-
log2(`${
|
|
4367
|
+
log2(`${pc18.cyan("seed")} ${file}`);
|
|
4142
4368
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
4143
4369
|
}
|
|
4144
|
-
log2(
|
|
4370
|
+
log2(pc18.green(`
|
|
4145
4371
|
Ran ${files.length} seed(s) against the local database.`));
|
|
4146
4372
|
} finally {
|
|
4147
4373
|
await local.dispose();
|
|
@@ -4252,9 +4478,9 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4252
4478
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4253
4479
|
const name = kebabCase4(rawName);
|
|
4254
4480
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
4255
|
-
const
|
|
4256
|
-
const path =
|
|
4257
|
-
if (
|
|
4481
|
+
const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
4482
|
+
const path = join23(appRoot, relative3);
|
|
4483
|
+
if (existsSync23(path)) throw new Error(`${relative3} already exists.`);
|
|
4258
4484
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4259
4485
|
let resource;
|
|
4260
4486
|
if (options2.resource) {
|
|
@@ -4263,15 +4489,15 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4263
4489
|
} else {
|
|
4264
4490
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
4265
4491
|
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
log2(`${
|
|
4492
|
+
mkdirSync12(join23(appRoot, SEEDS_DIR), { recursive: true });
|
|
4493
|
+
writeFileSync17(path, renderSeed(resource, readStack(appRoot)));
|
|
4494
|
+
log2(`${pc18.green("create".padEnd(9))} ${relative3}`);
|
|
4269
4495
|
log2(`
|
|
4270
|
-
Run it with ${
|
|
4271
|
-
return
|
|
4496
|
+
Run it with ${pc18.bold(`flare seed ${name}`)}.`);
|
|
4497
|
+
return relative3;
|
|
4272
4498
|
}
|
|
4273
4499
|
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4274
|
-
const runner =
|
|
4500
|
+
const runner = join23(appRoot, ".flare-seed-runner.mts");
|
|
4275
4501
|
const script = [
|
|
4276
4502
|
`import "dotenv/config";`,
|
|
4277
4503
|
`import { createFake } from "@flaredev/core";`,
|
|
@@ -4299,23 +4525,23 @@ async function runPrismaSeeds(appRoot, files, log2) {
|
|
|
4299
4525
|
`await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
|
|
4300
4526
|
""
|
|
4301
4527
|
].join("\n");
|
|
4302
|
-
|
|
4528
|
+
writeFileSync17(runner, script);
|
|
4303
4529
|
try {
|
|
4304
4530
|
const tsx = resolveBin(appRoot, "tsx", "tsx");
|
|
4305
4531
|
const code = await runNode([tsx, runner], appRoot);
|
|
4306
4532
|
if (code !== 0) throw new Error("A seed failed; nothing further was run.");
|
|
4307
|
-
log2(
|
|
4533
|
+
log2(pc18.green(`
|
|
4308
4534
|
Ran ${files.length} seed(s).`));
|
|
4309
4535
|
} finally {
|
|
4310
|
-
|
|
4536
|
+
rmSync5(runner, { force: true });
|
|
4311
4537
|
}
|
|
4312
4538
|
}
|
|
4313
4539
|
|
|
4314
4540
|
// src/commands/db-push.ts
|
|
4315
|
-
import { mkdtempSync as mkdtempSync2, rmSync as
|
|
4541
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
|
|
4316
4542
|
import { tmpdir as tmpdir2 } from "os";
|
|
4317
|
-
import { join as
|
|
4318
|
-
import
|
|
4543
|
+
import { join as join24 } from "path";
|
|
4544
|
+
import pc19 from "picocolors";
|
|
4319
4545
|
|
|
4320
4546
|
// src/terminal.ts
|
|
4321
4547
|
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
@@ -4379,7 +4605,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4379
4605
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4380
4606
|
const started = Date.now();
|
|
4381
4607
|
const local = await openLocalD1(appRoot, options2.database);
|
|
4382
|
-
const dir = mkdtempSync2(
|
|
4608
|
+
const dir = mkdtempSync2(join24(tmpdir2(), "flare-push-"));
|
|
4383
4609
|
try {
|
|
4384
4610
|
const statements = [];
|
|
4385
4611
|
const counts = [];
|
|
@@ -4398,33 +4624,33 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4398
4624
|
offset += PAGE;
|
|
4399
4625
|
}
|
|
4400
4626
|
counts.push({ table, rows });
|
|
4401
|
-
log2(`${
|
|
4627
|
+
log2(`${pc19.cyan("read")} ${table} ${pc19.dim(`(${formatCount(rows)} rows)`)}`);
|
|
4402
4628
|
}
|
|
4403
4629
|
const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
|
|
4404
4630
|
if (total === 0) {
|
|
4405
4631
|
log2("Nothing to push: those tables are empty locally.");
|
|
4406
4632
|
return { tables: counts, ms: Date.now() - started };
|
|
4407
4633
|
}
|
|
4408
|
-
const file =
|
|
4409
|
-
|
|
4634
|
+
const file = join24(dir, "push.sql");
|
|
4635
|
+
writeFileSync18(file, statements.join("\n"));
|
|
4410
4636
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4411
4637
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4412
4638
|
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
4413
4639
|
${result.output.trim()}`);
|
|
4414
4640
|
const ms = Date.now() - started;
|
|
4415
|
-
log2(`${
|
|
4641
|
+
log2(`${pc19.green("\u2714")} Pushed ${pc19.bold(formatCount(total))} rows to ${pc19.bold(db.database_name)} ${pc19.dim(`(${formatDuration(ms)})`)}`);
|
|
4416
4642
|
return { tables: counts, ms };
|
|
4417
4643
|
} finally {
|
|
4418
|
-
|
|
4644
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
4419
4645
|
await local.dispose();
|
|
4420
4646
|
}
|
|
4421
4647
|
}
|
|
4422
4648
|
|
|
4423
4649
|
// src/commands/seed-resource.ts
|
|
4424
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
4650
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
|
|
4425
4651
|
import { tmpdir as tmpdir3 } from "os";
|
|
4426
|
-
import { join as
|
|
4427
|
-
import
|
|
4652
|
+
import { join as join25 } from "path";
|
|
4653
|
+
import pc20 from "picocolors";
|
|
4428
4654
|
|
|
4429
4655
|
// src/seed/rows.ts
|
|
4430
4656
|
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
@@ -4608,13 +4834,13 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4608
4834
|
...options2,
|
|
4609
4835
|
onProgress: (written, total) => {
|
|
4610
4836
|
const elapsed = Date.now() - started;
|
|
4611
|
-
progress.update(` ${
|
|
4837
|
+
progress.update(` ${pc20.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc20.dim(formatRate(written, elapsed))}`);
|
|
4612
4838
|
}
|
|
4613
4839
|
});
|
|
4614
4840
|
progress.done();
|
|
4615
4841
|
const { rows, ms, where } = result;
|
|
4616
4842
|
console.log(
|
|
4617
|
-
`${
|
|
4843
|
+
`${pc20.green("\u2714")} Seeded ${pc20.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc20.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
|
|
4618
4844
|
);
|
|
4619
4845
|
return result;
|
|
4620
4846
|
} catch (error) {
|
|
@@ -4662,8 +4888,8 @@ ${result.output.trim()}`);
|
|
|
4662
4888
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4663
4889
|
startIndex = counted?.total ?? 0;
|
|
4664
4890
|
}
|
|
4665
|
-
const dir = mkdtempSync3(
|
|
4666
|
-
const file =
|
|
4891
|
+
const dir = mkdtempSync3(join25(tmpdir3(), "flare-seed-"));
|
|
4892
|
+
const file = join25(dir, `${resource.table}.sql`);
|
|
4667
4893
|
try {
|
|
4668
4894
|
const statements = [];
|
|
4669
4895
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4673,13 +4899,13 @@ ${result.output.trim()}`);
|
|
|
4673
4899
|
written += statement.rows;
|
|
4674
4900
|
options2.onProgress?.(written, total);
|
|
4675
4901
|
}
|
|
4676
|
-
|
|
4677
|
-
log2(`${
|
|
4902
|
+
writeFileSync19(file, statements.join("\n"));
|
|
4903
|
+
log2(`${pc20.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc20.bold(db.database_name)}`);
|
|
4678
4904
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4679
4905
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4680
4906
|
${result.output.trim()}`);
|
|
4681
4907
|
} finally {
|
|
4682
|
-
|
|
4908
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
4683
4909
|
}
|
|
4684
4910
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4685
4911
|
}
|
|
@@ -4706,17 +4932,17 @@ ${result.output.trim()}`);
|
|
|
4706
4932
|
|
|
4707
4933
|
// src/commands/billing-sync.ts
|
|
4708
4934
|
import { createRequire as createRequire2 } from "module";
|
|
4709
|
-
import { existsSync as
|
|
4935
|
+
import { existsSync as existsSync24, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
|
|
4710
4936
|
import { tmpdir as tmpdir4 } from "os";
|
|
4711
|
-
import { join as
|
|
4937
|
+
import { join as join26 } from "path";
|
|
4712
4938
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4713
4939
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4714
|
-
import
|
|
4940
|
+
import pc21 from "picocolors";
|
|
4715
4941
|
function stripeKeys(appRoot) {
|
|
4716
4942
|
const map = {};
|
|
4717
|
-
const path =
|
|
4718
|
-
if (
|
|
4719
|
-
for (const line of
|
|
4943
|
+
const path = join26(appRoot, ".dev.vars");
|
|
4944
|
+
if (existsSync24(path)) {
|
|
4945
|
+
for (const line of readFileSync16(path, "utf8").split(/\r?\n/)) {
|
|
4720
4946
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
4721
4947
|
if (match) map[match[1]] = match[2].trim();
|
|
4722
4948
|
}
|
|
@@ -4799,10 +5025,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4799
5025
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4800
5026
|
const db = readD1Databases(appRoot)[0];
|
|
4801
5027
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4802
|
-
const appRequire = createRequire2(
|
|
5028
|
+
const appRequire = createRequire2(join26(appRoot, "package.json"));
|
|
4803
5029
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4804
5030
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4805
|
-
const appName = JSON.parse(
|
|
5031
|
+
const appName = JSON.parse(readFileSync16(join26(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4806
5032
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4807
5033
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4808
5034
|
if (products.length === 0) {
|
|
@@ -4823,41 +5049,41 @@ async function syncPlans(options2 = {}) {
|
|
|
4823
5049
|
${existingResult.output.trim()}`);
|
|
4824
5050
|
}
|
|
4825
5051
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4826
|
-
const dir = mkdtempSync4(
|
|
5052
|
+
const dir = mkdtempSync4(join26(tmpdir4(), "flare-sync-"));
|
|
4827
5053
|
try {
|
|
4828
|
-
const file =
|
|
4829
|
-
|
|
5054
|
+
const file = join26(dir, "sync-plans.sql");
|
|
5055
|
+
writeFileSync20(file, renderSyncSql(rows));
|
|
4830
5056
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4831
5057
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4832
5058
|
${result.output.trim()}`);
|
|
4833
5059
|
} finally {
|
|
4834
|
-
|
|
5060
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
4835
5061
|
}
|
|
4836
5062
|
let created = 0;
|
|
4837
5063
|
for (const row of rows) {
|
|
4838
5064
|
const isNew = !known.has(row.stripePriceId);
|
|
4839
5065
|
if (isNew) created++;
|
|
4840
5066
|
const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
|
|
4841
|
-
log2(`${(isNew ?
|
|
5067
|
+
log2(`${(isNew ? pc21.green("create") : pc21.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc21.dim(price)}`);
|
|
4842
5068
|
}
|
|
4843
|
-
for (const name of skipped) log2(
|
|
5069
|
+
for (const name of skipped) log2(pc21.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
|
|
4844
5070
|
const configuration = await syncPortalConfiguration(stripe, rows, appName);
|
|
4845
|
-
if (configuration) log2(`${
|
|
4846
|
-
log2(
|
|
5071
|
+
if (configuration) log2(`${pc21.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
|
|
5072
|
+
log2(pc21.green(`
|
|
4847
5073
|
${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
|
|
4848
5074
|
return { created, updated: rows.length - created, skipped };
|
|
4849
5075
|
}
|
|
4850
5076
|
|
|
4851
5077
|
// src/commands/sync.ts
|
|
4852
|
-
import { existsSync as
|
|
4853
|
-
import { join as
|
|
4854
|
-
import
|
|
5078
|
+
import { existsSync as existsSync25 } from "fs";
|
|
5079
|
+
import { join as join27 } from "path";
|
|
5080
|
+
import pc22 from "picocolors";
|
|
4855
5081
|
async function syncTypes(options2 = {}) {
|
|
4856
5082
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4857
5083
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4858
5084
|
const all = await loadResources(appRoot);
|
|
4859
5085
|
const policies = await loadPolicies(appRoot);
|
|
4860
|
-
const tracksPolicies = policies.length > 0 ||
|
|
5086
|
+
const tracksPolicies = policies.length > 0 || existsSync25(join27(appRoot, "policies/index.ts"));
|
|
4861
5087
|
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
4862
5088
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4863
5089
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -4869,42 +5095,42 @@ async function syncTypes(options2 = {}) {
|
|
|
4869
5095
|
if (drift.length) {
|
|
4870
5096
|
log2(
|
|
4871
5097
|
`
|
|
4872
|
-
${
|
|
5098
|
+
${pc22.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
|
|
4873
5099
|
Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
|
|
4874
5100
|
);
|
|
4875
5101
|
}
|
|
4876
5102
|
if (unmarked.length) {
|
|
4877
5103
|
log2(`
|
|
4878
|
-
${
|
|
5104
|
+
${pc22.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
|
|
4879
5105
|
}
|
|
4880
5106
|
if (orphans.length) {
|
|
4881
5107
|
const resources = [...new Set(orphans.map((o) => o.resource))];
|
|
4882
5108
|
log2(`
|
|
4883
|
-
${
|
|
5109
|
+
${pc22.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
|
|
4884
5110
|
for (const orphan of orphans) log2(` ${orphan.path}`);
|
|
4885
5111
|
log2(
|
|
4886
|
-
`If the resource was deleted, remove them with ${
|
|
5112
|
+
`If the resource was deleted, remove them with ${pc22.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
|
|
4887
5113
|
);
|
|
4888
5114
|
}
|
|
4889
5115
|
const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
|
|
4890
5116
|
if (tablesChanged && !options2.check) {
|
|
4891
5117
|
log2(`
|
|
4892
|
-
${
|
|
5118
|
+
${pc22.cyan("Tables changed.")} Create a migration with ${pc22.bold("flare gen migration <name> --from-schema")}, then ${pc22.bold("flare migrate")}.`);
|
|
4893
5119
|
}
|
|
4894
5120
|
const problems = drift.length + unmarked.length + orphans.length;
|
|
4895
5121
|
if (options2.check) {
|
|
4896
5122
|
const stale = changed.length;
|
|
4897
|
-
if (stale === 0 && problems === 0) log2(
|
|
5123
|
+
if (stale === 0 && problems === 0) log2(pc22.green("Generated files are in sync with the resource descriptors."));
|
|
4898
5124
|
else log2(`
|
|
4899
|
-
${
|
|
5125
|
+
${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flare sync-types")}.`);
|
|
4900
5126
|
return stale + problems > 0 ? 1 : 0;
|
|
4901
5127
|
}
|
|
4902
|
-
if (changed.length === 0 && problems === 0) log2(
|
|
5128
|
+
if (changed.length === 0 && problems === 0) log2(pc22.green("Everything is in sync."));
|
|
4903
5129
|
return problems > 0 ? 1 : 0;
|
|
4904
5130
|
}
|
|
4905
5131
|
|
|
4906
5132
|
// src/commands/user-role.ts
|
|
4907
|
-
import
|
|
5133
|
+
import pc23 from "picocolors";
|
|
4908
5134
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
4909
5135
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
4910
5136
|
function userRoleSql(email, role) {
|
|
@@ -4918,8 +5144,8 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
4918
5144
|
const sql2 = userRoleSql(email, role);
|
|
4919
5145
|
const roles = await listRoles(options2);
|
|
4920
5146
|
if (roles && !roles.includes(role)) {
|
|
4921
|
-
log2(
|
|
4922
|
-
log2(
|
|
5147
|
+
log2(pc23.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
5148
|
+
log2(pc23.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
|
|
4923
5149
|
return 1;
|
|
4924
5150
|
}
|
|
4925
5151
|
const db = readD1Databases(appRoot)[0];
|
|
@@ -4933,31 +5159,31 @@ ${result.output.trim()}`);
|
|
|
4933
5159
|
const [first] = extractJson(result.output);
|
|
4934
5160
|
const updated = first?.results ?? [];
|
|
4935
5161
|
if (updated.length === 0) {
|
|
4936
|
-
log2(
|
|
5162
|
+
log2(pc23.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
|
|
4937
5163
|
return 1;
|
|
4938
5164
|
}
|
|
4939
|
-
log2(
|
|
5165
|
+
log2(pc23.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
4940
5166
|
return 0;
|
|
4941
5167
|
}
|
|
4942
5168
|
|
|
4943
5169
|
// src/utils/install.ts
|
|
4944
5170
|
import { spawn as spawn5 } from "child_process";
|
|
4945
|
-
import { existsSync as
|
|
4946
|
-
import { join as
|
|
5171
|
+
import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
|
|
5172
|
+
import { join as join28 } from "path";
|
|
4947
5173
|
function expectedPackages(dir) {
|
|
4948
|
-
const npmLock =
|
|
4949
|
-
if (
|
|
5174
|
+
const npmLock = join28(dir, "package-lock.json");
|
|
5175
|
+
if (existsSync26(npmLock)) {
|
|
4950
5176
|
try {
|
|
4951
|
-
const lock = JSON.parse(
|
|
5177
|
+
const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
|
|
4952
5178
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
4953
5179
|
} catch {
|
|
4954
5180
|
return 0;
|
|
4955
5181
|
}
|
|
4956
5182
|
}
|
|
4957
|
-
const pnpmLock =
|
|
4958
|
-
if (
|
|
5183
|
+
const pnpmLock = join28(dir, "pnpm-lock.yaml");
|
|
5184
|
+
if (existsSync26(pnpmLock)) {
|
|
4959
5185
|
try {
|
|
4960
|
-
const text =
|
|
5186
|
+
const text = readFileSync17(pnpmLock, "utf8");
|
|
4961
5187
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
4962
5188
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
4963
5189
|
} catch {
|
|
@@ -4967,15 +5193,15 @@ function expectedPackages(dir) {
|
|
|
4967
5193
|
return 0;
|
|
4968
5194
|
}
|
|
4969
5195
|
function installedPackages(dir) {
|
|
4970
|
-
const root =
|
|
4971
|
-
if (!
|
|
5196
|
+
const root = join28(dir, "node_modules");
|
|
5197
|
+
if (!existsSync26(root)) return 0;
|
|
4972
5198
|
let total = 0;
|
|
4973
5199
|
try {
|
|
4974
|
-
for (const entry of
|
|
5200
|
+
for (const entry of readdirSync10(root, { withFileTypes: true })) {
|
|
4975
5201
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4976
5202
|
if (entry.name.startsWith("@")) {
|
|
4977
5203
|
try {
|
|
4978
|
-
total +=
|
|
5204
|
+
total += readdirSync10(join28(root, entry.name)).length;
|
|
4979
5205
|
} catch {
|
|
4980
5206
|
}
|
|
4981
5207
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -5022,9 +5248,9 @@ function createCli() {
|
|
|
5022
5248
|
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
|
|
5023
5249
|
async (dir, options2) => {
|
|
5024
5250
|
const wanted = !options2.skipInstall;
|
|
5025
|
-
let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
5251
|
+
let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
5026
5252
|
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
|
|
5027
|
-
const result = createApp(dir, { pm: options2.pm,
|
|
5253
|
+
const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
|
|
5028
5254
|
let installed = false;
|
|
5029
5255
|
if (wanted) {
|
|
5030
5256
|
const spin = prompts3.spinner({ indicator: "timer" });
|
|
@@ -5036,7 +5262,7 @@ function createCli() {
|
|
|
5036
5262
|
});
|
|
5037
5263
|
installed = outcome.code === 0;
|
|
5038
5264
|
if (installed) {
|
|
5039
|
-
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${
|
|
5265
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc24.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
5040
5266
|
} else {
|
|
5041
5267
|
spin.error(`${result.packageManager} install failed`);
|
|
5042
5268
|
process.stderr.write(outcome.output);
|
|
@@ -5086,6 +5312,13 @@ function createCli() {
|
|
|
5086
5312
|
cli.command("seed:resource <resource> [count]", "Fill a resource's table with sample rows built from its descriptor").option("--count <rows>", "How many rows: 1000, 25k, 1m (default: 25)").option("--remote", "Seed the deployed database instead of the local one (needs --yes)").option("--truncate", "Delete the table's rows first").option("--seed <number>", "Same number, same rows").option("--env <name>", "Wrangler environment").option("--database <binding>", "D1 binding, when the app has several").option("-y, --yes", "Confirm writing to the remote database").example("flare seed:resource Contact --count 1000").example("flare seed:resource Contact 1m # a million rows").example("flare seed:resource Contact 5k --remote --yes").action(async (resource, count, options2) => {
|
|
5087
5313
|
await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
|
|
5088
5314
|
});
|
|
5315
|
+
cli.command("diff [filter]", "Show how the code Flare copied into this app (lib/resource) differs from the current version").example("flare diff").example('flare diff store # just the files whose path contains "store"').action((filter) => {
|
|
5316
|
+
diffTracked({ filter });
|
|
5317
|
+
});
|
|
5318
|
+
cli.command("update [filter]", "Replace this app's copies of Flare's files with the current version (discards your edits)").option("-y, --yes", "Confirm overwriting").example("flare update # lists what would change").example("flare update --yes # applies it").action((filter, options2) => {
|
|
5319
|
+
const code = updateTracked({ filter, yes: options2.yes });
|
|
5320
|
+
if (code !== 0) process.exitCode = code;
|
|
5321
|
+
});
|
|
5089
5322
|
cli.command("db:push [...tables]", "Copy rows from the local database to the deployed one (seeded catalogues, reference data)").option("--truncate", "Delete the remote rows of those tables first").option("--database <binding>", "D1 binding, when the app has several").option("--env <name>", "Wrangler environment").option("-y, --yes", "Confirm writing to the deployed database").example("flare db:push products categories --yes").example("flare db:push plans --truncate --yes").action(async (tables, options2) => {
|
|
5090
5323
|
await dbPush(tables, options2);
|
|
5091
5324
|
});
|
|
@@ -5134,6 +5367,6 @@ try {
|
|
|
5134
5367
|
}
|
|
5135
5368
|
}
|
|
5136
5369
|
} catch (error) {
|
|
5137
|
-
console.error(
|
|
5370
|
+
console.error(pc25.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
5138
5371
|
process.exitCode = 1;
|
|
5139
5372
|
}
|