@flaredev/cli 0.4.0 → 0.4.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 +194 -92
- package/package.json +2 -2
- package/templates/app/app/dashboard/actions.ts +38 -5
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +2 -2
- package/templates/app/components/dashboard/record-actions.tsx +2 -2
- package/templates/app/components/dashboard/record-detail.tsx +5 -2
- package/templates/app/components/dashboard/resource-form-dialog.tsx +3 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form.tsx +165 -165
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +8 -5
- package/templates/app/components/dashboard/row-actions.tsx +2 -2
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- package/templates/locks/yarn.lock +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import pc24 from "picocolors";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { cac } from "cac";
|
|
6
|
-
import
|
|
6
|
+
import pc23 from "picocolors";
|
|
7
7
|
import * as prompts3 from "@clack/prompts";
|
|
8
8
|
import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
|
|
9
9
|
|
|
@@ -139,6 +139,7 @@ function parseTheme(value) {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// src/versions.ts
|
|
142
|
+
var COMPATIBILITY_DATE = "2026-09-15";
|
|
142
143
|
var APP_DEPENDENCIES = {
|
|
143
144
|
"@better-auth/drizzle-adapter": "1.7.5",
|
|
144
145
|
"@better-auth/passkey": "1.7.5",
|
|
@@ -280,7 +281,7 @@ function createApp(target, options2 = {}) {
|
|
|
280
281
|
const authMethods = parseAuthMethods(options2.auth);
|
|
281
282
|
const theme = parseTheme(options2.theme);
|
|
282
283
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
283
|
-
const compatibilityDate = options2.compatibilityDate ??
|
|
284
|
+
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
284
285
|
const files = copyTemplate(join2(templatesDir, "app"), dir, {
|
|
285
286
|
APP_NAME: name,
|
|
286
287
|
COMPAT_DATE: compatibilityDate,
|
|
@@ -379,6 +380,20 @@ function splitTopLevel(input) {
|
|
|
379
380
|
parts.push(current);
|
|
380
381
|
return parts.map((part) => part.trim()).filter(Boolean);
|
|
381
382
|
}
|
|
383
|
+
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
384
|
+
var SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
|
|
385
|
+
function parseSize(text, spec) {
|
|
386
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i.exec(text.trim());
|
|
387
|
+
if (!match) {
|
|
388
|
+
throw new FieldGrammarError(`"${spec}": "${text}" isn't a size. Write it like "file:[image]:5mb".`);
|
|
389
|
+
}
|
|
390
|
+
const bytes = Math.round(Number(match[1]) * SIZE_UNITS[match[2].toLowerCase()]);
|
|
391
|
+
if (bytes < 1) throw new FieldGrammarError(`"${spec}": a size limit has to be at least one byte.`);
|
|
392
|
+
if (bytes > MAX_UPLOAD_BYTES) {
|
|
393
|
+
throw new FieldGrammarError(`"${spec}": ${text} is more than a Worker will carry in one request; the most is 100mb.`);
|
|
394
|
+
}
|
|
395
|
+
return bytes;
|
|
396
|
+
}
|
|
382
397
|
function editDistance(a, b) {
|
|
383
398
|
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
384
399
|
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
@@ -439,11 +454,14 @@ function parseField(spec) {
|
|
|
439
454
|
}
|
|
440
455
|
if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
|
|
441
456
|
field.options = options2;
|
|
442
|
-
} else if (
|
|
457
|
+
} else if (/^file\[/.test(type)) {
|
|
458
|
+
throw new FieldGrammarError(`"${spec}": file fields need a colon before the list, e.g. "${field.key}:file:[${type.slice(5, -1) || "image"}]".`);
|
|
459
|
+
} else if (match = /^file(?::\[(.*)\])?(?::([^:]*))?$/.exec(type)) {
|
|
443
460
|
field.kind = "file";
|
|
444
461
|
if (match[1] === void 0) {
|
|
445
462
|
throw new FieldGrammarError(`"${spec}": file fields list accepted types, e.g. "file:[image,pdf]".`);
|
|
446
463
|
}
|
|
464
|
+
if (match[2] !== void 0) field.maxBytes = parseSize(match[2], spec);
|
|
447
465
|
const categories = Object.keys(FILE_CATEGORIES);
|
|
448
466
|
const accept = list(match[1], "file type", spec).map((category) => category.toLowerCase());
|
|
449
467
|
for (const category of accept) {
|
|
@@ -592,6 +610,7 @@ function options(field) {
|
|
|
592
610
|
if (field.format) opts.format = field.format;
|
|
593
611
|
if (field.widget) opts.widget = field.widget;
|
|
594
612
|
if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
|
|
613
|
+
if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
|
|
595
614
|
return opts;
|
|
596
615
|
}
|
|
597
616
|
function toField(field) {
|
|
@@ -1334,7 +1353,7 @@ async function loadResources(root) {
|
|
|
1334
1353
|
const appRoot = resolve3(root);
|
|
1335
1354
|
const dir = join3(appRoot, "resources");
|
|
1336
1355
|
if (!existsSync4(dir)) return [];
|
|
1337
|
-
const jiti = createJiti(join3(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1356
|
+
const jiti = createJiti(join3(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1338
1357
|
const loaded = [];
|
|
1339
1358
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1340
1359
|
const path = join3(dir, file);
|
|
@@ -1534,6 +1553,7 @@ function parseDeployArgs(args) {
|
|
|
1534
1553
|
if (arg === "--skip-migrations") skipMigrations = true;
|
|
1535
1554
|
else if (arg === "--skip-secrets") skipSecrets = true;
|
|
1536
1555
|
else if (arg === "--skip-security") skipSecurity = true;
|
|
1556
|
+
else if (arg === "--yes" || arg === "-y") continue;
|
|
1537
1557
|
else {
|
|
1538
1558
|
forwarded.push(arg);
|
|
1539
1559
|
if (arg === "--env" && args[i + 1]) env = args[i + 1];
|
|
@@ -3954,12 +3974,121 @@ Run it with ${pc17.bold(`flare seed ${name}`)}.`);
|
|
|
3954
3974
|
return relative2;
|
|
3955
3975
|
}
|
|
3956
3976
|
|
|
3957
|
-
// src/commands/
|
|
3977
|
+
// src/commands/db-push.ts
|
|
3958
3978
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
3959
3979
|
import { tmpdir as tmpdir2 } from "os";
|
|
3960
3980
|
import { join as join22 } from "path";
|
|
3961
3981
|
import pc18 from "picocolors";
|
|
3962
3982
|
|
|
3983
|
+
// src/terminal.ts
|
|
3984
|
+
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
3985
|
+
function formatDuration(ms) {
|
|
3986
|
+
const seconds = Math.round(ms / 1e3);
|
|
3987
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3988
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
3989
|
+
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
3990
|
+
}
|
|
3991
|
+
var formatCount = (value) => value.toLocaleString();
|
|
3992
|
+
function formatRate(count, ms, unit = "rows") {
|
|
3993
|
+
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
3994
|
+
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
3995
|
+
}
|
|
3996
|
+
function progressLine(everyMs = 120) {
|
|
3997
|
+
if (!isTTY()) return { update: () => {
|
|
3998
|
+
}, done: () => {
|
|
3999
|
+
} };
|
|
4000
|
+
let last = 0;
|
|
4001
|
+
let width = 0;
|
|
4002
|
+
const write = (message) => {
|
|
4003
|
+
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
4004
|
+
width = Math.max(width, message.length);
|
|
4005
|
+
};
|
|
4006
|
+
return {
|
|
4007
|
+
update(message) {
|
|
4008
|
+
const now = Date.now();
|
|
4009
|
+
if (now - last < everyMs) return;
|
|
4010
|
+
last = now;
|
|
4011
|
+
write(message);
|
|
4012
|
+
},
|
|
4013
|
+
done() {
|
|
4014
|
+
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
4015
|
+
width = 0;
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
// src/commands/db-push.ts
|
|
4021
|
+
var PAGE = 2e3;
|
|
4022
|
+
function pickDatabase(databases, wanted) {
|
|
4023
|
+
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4024
|
+
if (wanted) {
|
|
4025
|
+
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
4026
|
+
if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
|
|
4027
|
+
return match;
|
|
4028
|
+
}
|
|
4029
|
+
if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
|
|
4030
|
+
return databases[0];
|
|
4031
|
+
}
|
|
4032
|
+
async function dbPush(tables, options2 = {}) {
|
|
4033
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4034
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4035
|
+
if (tables.length === 0) throw new Error("Name at least one table, e.g. flare db:push products categories --yes");
|
|
4036
|
+
const bad = tables.find((table) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(table));
|
|
4037
|
+
if (bad) throw new Error(`"${bad}" isn't a table name.`);
|
|
4038
|
+
if (!options2.yes) {
|
|
4039
|
+
throw new Error(`This writes ${tables.join(", ")} to the deployed database. Re-run with --yes to confirm.`);
|
|
4040
|
+
}
|
|
4041
|
+
const db = pickDatabase(readD1Databases(appRoot), options2.database);
|
|
4042
|
+
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4043
|
+
const started = Date.now();
|
|
4044
|
+
const local = await openLocalD1(appRoot, options2.database);
|
|
4045
|
+
const dir = mkdtempSync2(join22(tmpdir2(), "flare-push-"));
|
|
4046
|
+
try {
|
|
4047
|
+
const statements = [];
|
|
4048
|
+
const counts = [];
|
|
4049
|
+
for (const table of tables) {
|
|
4050
|
+
if (options2.truncate) statements.push(`DELETE FROM "${table}";`);
|
|
4051
|
+
let offset = 0;
|
|
4052
|
+
let rows = 0;
|
|
4053
|
+
for (; ; ) {
|
|
4054
|
+
const page = await local.db.prepare(`SELECT * FROM "${table}" LIMIT ${PAGE} OFFSET ${offset}`).all();
|
|
4055
|
+
const results = page.results ?? [];
|
|
4056
|
+
if (results.length === 0) break;
|
|
4057
|
+
const columns = Object.keys(results[0]);
|
|
4058
|
+
for (const statement of insertStatements(table, columns, results)) statements.push(statement.sql);
|
|
4059
|
+
rows += results.length;
|
|
4060
|
+
if (results.length < PAGE) break;
|
|
4061
|
+
offset += PAGE;
|
|
4062
|
+
}
|
|
4063
|
+
counts.push({ table, rows });
|
|
4064
|
+
log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
|
|
4065
|
+
}
|
|
4066
|
+
const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
|
|
4067
|
+
if (total === 0) {
|
|
4068
|
+
log2("Nothing to push: those tables are empty locally.");
|
|
4069
|
+
return { tables: counts, ms: Date.now() - started };
|
|
4070
|
+
}
|
|
4071
|
+
const file = join22(dir, "push.sql");
|
|
4072
|
+
writeFileSync15(file, statements.join("\n"));
|
|
4073
|
+
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4074
|
+
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4075
|
+
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
4076
|
+
${result.output.trim()}`);
|
|
4077
|
+
const ms = Date.now() - started;
|
|
4078
|
+
log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
|
|
4079
|
+
return { tables: counts, ms };
|
|
4080
|
+
} finally {
|
|
4081
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
4082
|
+
await local.dispose();
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
// src/commands/seed-resource.ts
|
|
4087
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
|
|
4088
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
4089
|
+
import { join as join23 } from "path";
|
|
4090
|
+
import pc19 from "picocolors";
|
|
4091
|
+
|
|
3963
4092
|
// src/seed/rows.ts
|
|
3964
4093
|
import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
|
|
3965
4094
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
@@ -3981,7 +4110,9 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3981
4110
|
return fake.slug();
|
|
3982
4111
|
}
|
|
3983
4112
|
const name = key.toLowerCase();
|
|
3984
|
-
const
|
|
4113
|
+
const resourceName2 = resource.name.toLowerCase();
|
|
4114
|
+
const organisation = has(resourceName2, "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
|
|
4115
|
+
const catalogue = has(resourceName2, "product", "item", "article", "sku", "listing", "merch", "book", "course", "plan", "service");
|
|
3985
4116
|
let value;
|
|
3986
4117
|
if (has(name, "email")) value = fake.email();
|
|
3987
4118
|
else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
|
|
@@ -3996,7 +4127,7 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3996
4127
|
else if (has(name, "colour", "color")) value = fake.color();
|
|
3997
4128
|
else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
|
|
3998
4129
|
else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
|
|
3999
|
-
else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
|
|
4130
|
+
else if (has(name, "name")) value = organisation ? fake.company() : catalogue ? fake.product(digitalish(resource)) : fake.fullName();
|
|
4000
4131
|
else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
|
|
4001
4132
|
else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
|
|
4002
4133
|
if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
|
|
@@ -4058,6 +4189,11 @@ function uniquify(value, row) {
|
|
|
4058
4189
|
if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
|
|
4059
4190
|
return `${value} ${row}`;
|
|
4060
4191
|
}
|
|
4192
|
+
function digitalish(resource) {
|
|
4193
|
+
return Object.entries(resource.fields).some(
|
|
4194
|
+
([key, def]) => def.kind === "enum" && /kind|type/.test(key) && def.options.some((option) => /digital|download|licence|license/i.test(option))
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4061
4197
|
function seedColumns(resource) {
|
|
4062
4198
|
return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
|
|
4063
4199
|
}
|
|
@@ -4082,43 +4218,6 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4082
4218
|
}
|
|
4083
4219
|
}
|
|
4084
4220
|
|
|
4085
|
-
// src/terminal.ts
|
|
4086
|
-
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
4087
|
-
function formatDuration(ms) {
|
|
4088
|
-
const seconds = Math.round(ms / 1e3);
|
|
4089
|
-
if (seconds < 60) return `${seconds}s`;
|
|
4090
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
4091
|
-
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
4092
|
-
}
|
|
4093
|
-
var formatCount = (value) => value.toLocaleString();
|
|
4094
|
-
function formatRate(count, ms, unit = "rows") {
|
|
4095
|
-
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
4096
|
-
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
4097
|
-
}
|
|
4098
|
-
function progressLine(everyMs = 120) {
|
|
4099
|
-
if (!isTTY()) return { update: () => {
|
|
4100
|
-
}, done: () => {
|
|
4101
|
-
} };
|
|
4102
|
-
let last = 0;
|
|
4103
|
-
let width = 0;
|
|
4104
|
-
const write = (message) => {
|
|
4105
|
-
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
4106
|
-
width = Math.max(width, message.length);
|
|
4107
|
-
};
|
|
4108
|
-
return {
|
|
4109
|
-
update(message) {
|
|
4110
|
-
const now = Date.now();
|
|
4111
|
-
if (now - last < everyMs) return;
|
|
4112
|
-
last = now;
|
|
4113
|
-
write(message);
|
|
4114
|
-
},
|
|
4115
|
-
done() {
|
|
4116
|
-
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
4117
|
-
width = 0;
|
|
4118
|
-
}
|
|
4119
|
-
};
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
4221
|
// src/commands/seed-resource.ts
|
|
4123
4222
|
async function countRows(db, table) {
|
|
4124
4223
|
const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
|
|
@@ -4143,7 +4242,7 @@ function pickResource(resources, name) {
|
|
|
4143
4242
|
}
|
|
4144
4243
|
return match;
|
|
4145
4244
|
}
|
|
4146
|
-
function
|
|
4245
|
+
function pickDatabase2(databases, wanted) {
|
|
4147
4246
|
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4148
4247
|
if (wanted) {
|
|
4149
4248
|
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
@@ -4172,13 +4271,13 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4172
4271
|
...options2,
|
|
4173
4272
|
onProgress: (written, total) => {
|
|
4174
4273
|
const elapsed = Date.now() - started;
|
|
4175
|
-
progress.update(` ${
|
|
4274
|
+
progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
|
|
4176
4275
|
}
|
|
4177
4276
|
});
|
|
4178
4277
|
progress.done();
|
|
4179
4278
|
const { rows, ms, where } = result;
|
|
4180
4279
|
console.log(
|
|
4181
|
-
`${
|
|
4280
|
+
`${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
|
|
4182
4281
|
);
|
|
4183
4282
|
return result;
|
|
4184
4283
|
} catch (error) {
|
|
@@ -4202,7 +4301,7 @@ async function seedResource(name, options2 = {}) {
|
|
|
4202
4301
|
let startIndex = 0;
|
|
4203
4302
|
const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
|
|
4204
4303
|
if (options2.remote) {
|
|
4205
|
-
const db =
|
|
4304
|
+
const db = pickDatabase2(readD1Databases(appRoot), options2.database);
|
|
4206
4305
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4207
4306
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4208
4307
|
const query = async (sql2) => {
|
|
@@ -4221,8 +4320,8 @@ ${result.output.trim()}`);
|
|
|
4221
4320
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4222
4321
|
startIndex = counted?.total ?? 0;
|
|
4223
4322
|
}
|
|
4224
|
-
const dir =
|
|
4225
|
-
const file =
|
|
4323
|
+
const dir = mkdtempSync3(join23(tmpdir3(), "flare-seed-"));
|
|
4324
|
+
const file = join23(dir, `${resource.table}.sql`);
|
|
4226
4325
|
try {
|
|
4227
4326
|
const statements = [];
|
|
4228
4327
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4232,13 +4331,13 @@ ${result.output.trim()}`);
|
|
|
4232
4331
|
written += statement.rows;
|
|
4233
4332
|
options2.onProgress?.(written, total);
|
|
4234
4333
|
}
|
|
4235
|
-
|
|
4236
|
-
log2(`${
|
|
4334
|
+
writeFileSync16(file, statements.join("\n"));
|
|
4335
|
+
log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
|
|
4237
4336
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4238
4337
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4239
4338
|
${result.output.trim()}`);
|
|
4240
4339
|
} finally {
|
|
4241
|
-
|
|
4340
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
4242
4341
|
}
|
|
4243
4342
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4244
4343
|
}
|
|
@@ -4265,15 +4364,15 @@ ${result.output.trim()}`);
|
|
|
4265
4364
|
|
|
4266
4365
|
// src/commands/billing-sync.ts
|
|
4267
4366
|
import { createRequire as createRequire2 } from "module";
|
|
4268
|
-
import { existsSync as existsSync22, mkdtempSync as
|
|
4269
|
-
import { tmpdir as
|
|
4270
|
-
import { join as
|
|
4367
|
+
import { existsSync as existsSync22, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
4368
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
4369
|
+
import { join as join24 } from "path";
|
|
4271
4370
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4272
4371
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4273
|
-
import
|
|
4372
|
+
import pc20 from "picocolors";
|
|
4274
4373
|
function stripeKeys(appRoot) {
|
|
4275
4374
|
const map = {};
|
|
4276
|
-
const path =
|
|
4375
|
+
const path = join24(appRoot, ".dev.vars");
|
|
4277
4376
|
if (existsSync22(path)) {
|
|
4278
4377
|
for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
|
|
4279
4378
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
@@ -4358,10 +4457,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4358
4457
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4359
4458
|
const db = readD1Databases(appRoot)[0];
|
|
4360
4459
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4361
|
-
const appRequire = createRequire2(
|
|
4460
|
+
const appRequire = createRequire2(join24(appRoot, "package.json"));
|
|
4362
4461
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4363
4462
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4364
|
-
const appName = JSON.parse(readFileSync13(
|
|
4463
|
+
const appName = JSON.parse(readFileSync13(join24(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4365
4464
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4366
4465
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4367
4466
|
if (products.length === 0) {
|
|
@@ -4382,41 +4481,41 @@ async function syncPlans(options2 = {}) {
|
|
|
4382
4481
|
${existingResult.output.trim()}`);
|
|
4383
4482
|
}
|
|
4384
4483
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4385
|
-
const dir =
|
|
4484
|
+
const dir = mkdtempSync4(join24(tmpdir4(), "flare-sync-"));
|
|
4386
4485
|
try {
|
|
4387
|
-
const file =
|
|
4388
|
-
|
|
4486
|
+
const file = join24(dir, "sync-plans.sql");
|
|
4487
|
+
writeFileSync17(file, renderSyncSql(rows));
|
|
4389
4488
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4390
4489
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4391
4490
|
${result.output.trim()}`);
|
|
4392
4491
|
} finally {
|
|
4393
|
-
|
|
4492
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
4394
4493
|
}
|
|
4395
4494
|
let created = 0;
|
|
4396
4495
|
for (const row of rows) {
|
|
4397
4496
|
const isNew = !known.has(row.stripePriceId);
|
|
4398
4497
|
if (isNew) created++;
|
|
4399
4498
|
const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
|
|
4400
|
-
log2(`${(isNew ?
|
|
4499
|
+
log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
|
|
4401
4500
|
}
|
|
4402
|
-
for (const name of skipped) log2(
|
|
4501
|
+
for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
|
|
4403
4502
|
const configuration = await syncPortalConfiguration(stripe, rows, appName);
|
|
4404
|
-
if (configuration) log2(`${
|
|
4405
|
-
log2(
|
|
4503
|
+
if (configuration) log2(`${pc20.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
|
|
4504
|
+
log2(pc20.green(`
|
|
4406
4505
|
${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
|
|
4407
4506
|
return { created, updated: rows.length - created, skipped };
|
|
4408
4507
|
}
|
|
4409
4508
|
|
|
4410
4509
|
// src/commands/sync.ts
|
|
4411
4510
|
import { existsSync as existsSync23 } from "fs";
|
|
4412
|
-
import { join as
|
|
4413
|
-
import
|
|
4511
|
+
import { join as join25 } from "path";
|
|
4512
|
+
import pc21 from "picocolors";
|
|
4414
4513
|
async function syncTypes(options2 = {}) {
|
|
4415
4514
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4416
4515
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4417
4516
|
const all = await loadResources(appRoot);
|
|
4418
4517
|
const policies = await loadPolicies(appRoot);
|
|
4419
|
-
const tracksPolicies = policies.length > 0 || existsSync23(
|
|
4518
|
+
const tracksPolicies = policies.length > 0 || existsSync23(join25(appRoot, "policies/index.ts"));
|
|
4420
4519
|
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
4421
4520
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4422
4521
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -4428,42 +4527,42 @@ async function syncTypes(options2 = {}) {
|
|
|
4428
4527
|
if (drift.length) {
|
|
4429
4528
|
log2(
|
|
4430
4529
|
`
|
|
4431
|
-
${
|
|
4530
|
+
${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
|
|
4432
4531
|
Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
|
|
4433
4532
|
);
|
|
4434
4533
|
}
|
|
4435
4534
|
if (unmarked.length) {
|
|
4436
4535
|
log2(`
|
|
4437
|
-
${
|
|
4536
|
+
${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
|
|
4438
4537
|
}
|
|
4439
4538
|
if (orphans.length) {
|
|
4440
4539
|
const resources = [...new Set(orphans.map((o) => o.resource))];
|
|
4441
4540
|
log2(`
|
|
4442
|
-
${
|
|
4541
|
+
${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
|
|
4443
4542
|
for (const orphan of orphans) log2(` ${orphan.path}`);
|
|
4444
4543
|
log2(
|
|
4445
|
-
`If the resource was deleted, remove them with ${
|
|
4544
|
+
`If the resource was deleted, remove them with ${pc21.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
|
|
4446
4545
|
);
|
|
4447
4546
|
}
|
|
4448
4547
|
const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
|
|
4449
4548
|
if (tablesChanged && !options2.check) {
|
|
4450
4549
|
log2(`
|
|
4451
|
-
${
|
|
4550
|
+
${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
|
|
4452
4551
|
}
|
|
4453
4552
|
const problems = drift.length + unmarked.length + orphans.length;
|
|
4454
4553
|
if (options2.check) {
|
|
4455
4554
|
const stale = changed.length;
|
|
4456
|
-
if (stale === 0 && problems === 0) log2(
|
|
4555
|
+
if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
|
|
4457
4556
|
else log2(`
|
|
4458
|
-
${
|
|
4557
|
+
${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
|
|
4459
4558
|
return stale + problems > 0 ? 1 : 0;
|
|
4460
4559
|
}
|
|
4461
|
-
if (changed.length === 0 && problems === 0) log2(
|
|
4560
|
+
if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
|
|
4462
4561
|
return problems > 0 ? 1 : 0;
|
|
4463
4562
|
}
|
|
4464
4563
|
|
|
4465
4564
|
// src/commands/user-role.ts
|
|
4466
|
-
import
|
|
4565
|
+
import pc22 from "picocolors";
|
|
4467
4566
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
4468
4567
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
4469
4568
|
function userRoleSql(email, role) {
|
|
@@ -4477,8 +4576,8 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
4477
4576
|
const sql2 = userRoleSql(email, role);
|
|
4478
4577
|
const roles = await listRoles(options2);
|
|
4479
4578
|
if (roles && !roles.includes(role)) {
|
|
4480
|
-
log2(
|
|
4481
|
-
log2(
|
|
4579
|
+
log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
4580
|
+
log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
|
|
4482
4581
|
return 1;
|
|
4483
4582
|
}
|
|
4484
4583
|
const db = readD1Databases(appRoot)[0];
|
|
@@ -4492,19 +4591,19 @@ ${result.output.trim()}`);
|
|
|
4492
4591
|
const [first] = extractJson(result.output);
|
|
4493
4592
|
const updated = first?.results ?? [];
|
|
4494
4593
|
if (updated.length === 0) {
|
|
4495
|
-
log2(
|
|
4594
|
+
log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
|
|
4496
4595
|
return 1;
|
|
4497
4596
|
}
|
|
4498
|
-
log2(
|
|
4597
|
+
log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
4499
4598
|
return 0;
|
|
4500
4599
|
}
|
|
4501
4600
|
|
|
4502
4601
|
// src/utils/install.ts
|
|
4503
4602
|
import { spawn as spawn5 } from "child_process";
|
|
4504
4603
|
import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
|
|
4505
|
-
import { join as
|
|
4604
|
+
import { join as join26 } from "path";
|
|
4506
4605
|
function expectedPackages(dir) {
|
|
4507
|
-
const npmLock =
|
|
4606
|
+
const npmLock = join26(dir, "package-lock.json");
|
|
4508
4607
|
if (existsSync24(npmLock)) {
|
|
4509
4608
|
try {
|
|
4510
4609
|
const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
|
|
@@ -4513,7 +4612,7 @@ function expectedPackages(dir) {
|
|
|
4513
4612
|
return 0;
|
|
4514
4613
|
}
|
|
4515
4614
|
}
|
|
4516
|
-
const pnpmLock =
|
|
4615
|
+
const pnpmLock = join26(dir, "pnpm-lock.yaml");
|
|
4517
4616
|
if (existsSync24(pnpmLock)) {
|
|
4518
4617
|
try {
|
|
4519
4618
|
const text = readFileSync14(pnpmLock, "utf8");
|
|
@@ -4526,7 +4625,7 @@ function expectedPackages(dir) {
|
|
|
4526
4625
|
return 0;
|
|
4527
4626
|
}
|
|
4528
4627
|
function installedPackages(dir) {
|
|
4529
|
-
const root =
|
|
4628
|
+
const root = join26(dir, "node_modules");
|
|
4530
4629
|
if (!existsSync24(root)) return 0;
|
|
4531
4630
|
let total = 0;
|
|
4532
4631
|
try {
|
|
@@ -4534,7 +4633,7 @@ function installedPackages(dir) {
|
|
|
4534
4633
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4535
4634
|
if (entry.name.startsWith("@")) {
|
|
4536
4635
|
try {
|
|
4537
|
-
total += readdirSync9(
|
|
4636
|
+
total += readdirSync9(join26(root, entry.name)).length;
|
|
4538
4637
|
} catch {
|
|
4539
4638
|
}
|
|
4540
4639
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -4594,7 +4693,7 @@ function createCli() {
|
|
|
4594
4693
|
});
|
|
4595
4694
|
installed = outcome.code === 0;
|
|
4596
4695
|
if (installed) {
|
|
4597
|
-
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${
|
|
4696
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
4598
4697
|
} else {
|
|
4599
4698
|
spin.error(`${result.packageManager} install failed`);
|
|
4600
4699
|
process.stderr.write(outcome.output);
|
|
@@ -4643,6 +4742,9 @@ function createCli() {
|
|
|
4643
4742
|
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) => {
|
|
4644
4743
|
await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
|
|
4645
4744
|
});
|
|
4745
|
+
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) => {
|
|
4746
|
+
await dbPush(tables, options2);
|
|
4747
|
+
});
|
|
4646
4748
|
cli.command("seed:make <name>", "Create seeds/<name>.seed.ts (with example rows for a matching resource)").option("--resource <name>", "Resource to write example rows for").action(async (name, options2) => {
|
|
4647
4749
|
await makeSeed(name, options2);
|
|
4648
4750
|
});
|
|
@@ -4688,6 +4790,6 @@ try {
|
|
|
4688
4790
|
}
|
|
4689
4791
|
}
|
|
4690
4792
|
} catch (error) {
|
|
4691
|
-
console.error(
|
|
4793
|
+
console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
4692
4794
|
process.exitCode = 1;
|
|
4693
4795
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flaredev/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "The flare command-line tool: create, generate, migrate, and deploy Flare apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Muke Johnbaptist",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
42
|
"jsonc-parser": "^3.3.1",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
|
-
"@flaredev/core": "0.4.
|
|
44
|
+
"@flaredev/core": "0.4.1"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22"
|