@flaredev/cli 0.1.2 → 0.2.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/dist/index.js +340 -163
- package/package.json +3 -2
- package/templates/app/app/api/openapi.json/route.ts +23 -0
- package/templates/app/app/api/reference/route.ts +36 -0
- package/templates/app/app/dashboard/account/page.tsx +38 -0
- package/templates/app/app/dashboard/layout.tsx +39 -0
- package/templates/app/app/dashboard/page.tsx +42 -9
- package/templates/app/app/forgot-password/page.tsx +22 -0
- package/templates/app/app/globals.css +109 -4
- package/templates/app/app/layout.tsx +28 -26
- package/templates/app/app/page.tsx +126 -13
- package/templates/app/app/reset-password/page.tsx +31 -0
- package/templates/app/app/sign-in/page.tsx +49 -33
- package/templates/app/app/sign-up/page.tsx +42 -23
- package/templates/app/app/two-factor/page.tsx +25 -0
- package/templates/app/components/account/security-settings.tsx +525 -0
- package/templates/app/components/admin/fields/country-field.tsx +152 -0
- package/templates/app/components/admin/fields/field-widget.tsx +186 -128
- package/templates/app/components/admin/fields/multi-select-field.tsx +46 -0
- package/templates/app/components/admin/fields/phone-field.tsx +86 -0
- package/templates/app/components/admin/resource-nav.tsx +120 -109
- package/templates/app/components/admin/resource-table.tsx +31 -1
- package/templates/app/components/auth/auth-shell.tsx +284 -0
- package/templates/app/components/auth/password-forms.tsx +79 -0
- package/templates/app/components/auth/provider-icons.tsx +39 -0
- package/templates/app/components/auth/sign-in-flow.tsx +315 -0
- package/templates/app/components/auth/sign-up-form.tsx +101 -0
- package/templates/app/components/auth/social-buttons.tsx +56 -0
- package/templates/app/components/auth/two-factor-form.tsx +124 -0
- package/templates/app/components/auth/ui.tsx +96 -0
- package/templates/app/components/dashboard/dashboard-nav.tsx +35 -0
- package/templates/app/components/marketing/product-preview.tsx +70 -0
- package/templates/app/components/marketing/site-header.tsx +39 -0
- package/templates/app/components/sign-out-button.tsx +1 -1
- package/templates/app/components/ui/checkbox.tsx +25 -0
- package/templates/app/components/ui/radio-group.tsx +29 -0
- package/templates/app/db/auth-schema.ts +167 -118
- package/templates/app/lib/admin-nav.ts +1 -1
- package/templates/app/lib/api-docs.ts +16 -0
- package/templates/app/lib/auth-client.ts +16 -1
- package/templates/app/lib/auth-config.ts +33 -0
- package/templates/app/lib/auth-emails.ts +94 -0
- package/templates/app/lib/auth.ts +81 -6
- package/templates/app/lib/site.ts +47 -0
- package/templates/app/lib/theme.ts +79 -0
- package/templates/app/migrations/0000_init.sql +32 -1
- package/templates/app/migrations/meta/0000_snapshot.json +215 -1
- package/templates/app/migrations/meta/_journal.json +1 -1
- package/templates/app/vite.config.ts +5 -3
- package/templates/app/components/auth-form.tsx +0 -120
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import pc19 from "picocolors";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { cac } from "cac";
|
|
@@ -14,34 +14,85 @@ import { FLARE_VERSION } from "@flaredev/core";
|
|
|
14
14
|
|
|
15
15
|
// src/auth-providers.ts
|
|
16
16
|
var AUTH_PROVIDERS = {
|
|
17
|
-
google: { envPrefix: "GOOGLE", console: "https://console.cloud.google.com/apis/credentials" },
|
|
18
|
-
github: { envPrefix: "GITHUB", console: "https://github.com/settings/developers" }
|
|
17
|
+
google: { label: "Google", envPrefix: "GOOGLE", console: "https://console.cloud.google.com/apis/credentials", extra: [] },
|
|
18
|
+
github: { label: "GitHub", envPrefix: "GITHUB", console: "https://github.com/settings/developers", extra: [] },
|
|
19
|
+
apple: {
|
|
20
|
+
label: "Apple",
|
|
21
|
+
envPrefix: "APPLE",
|
|
22
|
+
console: "https://developer.apple.com/account/resources/identifiers/list/serviceId",
|
|
23
|
+
// The client secret is a JWT signed with your key; the bundle id is only for native apps.
|
|
24
|
+
extra: ["# APPLE_CLIENT_SECRET is a JWT you generate from your Sign in with Apple key (see the auth guide).", "APPLE_APP_BUNDLE_ID="]
|
|
25
|
+
},
|
|
26
|
+
microsoft: {
|
|
27
|
+
label: "Microsoft",
|
|
28
|
+
envPrefix: "MICROSOFT",
|
|
29
|
+
console: "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade",
|
|
30
|
+
extra: ['# "common" (the default) accepts personal and work accounts; a tenant id limits sign-in to one organisation.', "MICROSOFT_TENANT_ID="]
|
|
31
|
+
}
|
|
19
32
|
};
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
var AUTH_METHODS = {
|
|
34
|
+
"magic-link": "Magic links (a sign-in link by email)",
|
|
35
|
+
"email-otp": "Email codes (a 6-digit sign-in code)",
|
|
36
|
+
passkeys: "Passkeys (Face ID, Touch ID, Windows Hello)",
|
|
37
|
+
"2fa-app": "Two-factor with an authenticator app, plus backup codes",
|
|
38
|
+
"2fa-email": "Two-factor with codes by email"
|
|
39
|
+
};
|
|
40
|
+
var DEFAULT_AUTH_METHODS = ["magic-link", "email-otp", "passkeys", "2fa-app", "2fa-email"];
|
|
41
|
+
function parseList(input, known, what) {
|
|
22
42
|
const names = input.split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
|
|
23
|
-
const unknown = names.filter((name) => !(name
|
|
43
|
+
const unknown = names.filter((name) => !known.includes(name));
|
|
24
44
|
if (unknown.length > 0) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Unknown auth provider${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Supported: ${Object.keys(AUTH_PROVIDERS).join(", ")}.`
|
|
27
|
-
);
|
|
45
|
+
throw new Error(`Unknown ${what}${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Supported: ${known.join(", ")}.`);
|
|
28
46
|
}
|
|
29
47
|
return [...new Set(names)];
|
|
30
48
|
}
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
function parseAuthProviders(input) {
|
|
50
|
+
if (!input) return [];
|
|
51
|
+
return parseList(input, Object.keys(AUTH_PROVIDERS), "auth provider");
|
|
52
|
+
}
|
|
53
|
+
function parseAuthMethods(input) {
|
|
54
|
+
if (input === void 0) return DEFAULT_AUTH_METHODS;
|
|
55
|
+
const value = input.trim().toLowerCase();
|
|
56
|
+
if (value === "all") return [...DEFAULT_AUTH_METHODS];
|
|
57
|
+
if (value === "none" || value === "") return [];
|
|
58
|
+
return parseList(value, Object.keys(AUTH_METHODS), "sign-in method");
|
|
59
|
+
}
|
|
60
|
+
function renderAuthConfig(methods, providers) {
|
|
61
|
+
const on = (method) => methods.includes(method);
|
|
62
|
+
return `/**
|
|
63
|
+
* How people sign in to this app. \`flare create\` wrote the choices you made; change
|
|
64
|
+
* them here any time. Every method's tables already exist, so switching one on or off
|
|
65
|
+
* needs no migration, and a method that's off is refused by the API, not just hidden.
|
|
66
|
+
*
|
|
67
|
+
* Email and password sign-in is always available.
|
|
68
|
+
*/
|
|
69
|
+
export const authConfig = {
|
|
70
|
+
/** A sign-in link by email. */
|
|
71
|
+
magicLink: ${on("magic-link")},
|
|
72
|
+
/** A 6-digit sign-in code by email. */
|
|
73
|
+
emailOtp: ${on("email-otp")},
|
|
74
|
+
/** Face ID, Touch ID, Windows Hello or a security key. */
|
|
75
|
+
passkeys: ${on("passkeys")},
|
|
76
|
+
twoFactor: {
|
|
77
|
+
/** Codes from an authenticator app (TOTP), with backup codes. */
|
|
78
|
+
authenticator: ${on("2fa-app")},
|
|
79
|
+
/** Codes by email as the second step. */
|
|
80
|
+
email: ${on("2fa-email")},
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Social sign-in. Each provider also needs its credentials (see .dev.vars.example);
|
|
84
|
+
* without them its button stays hidden.
|
|
85
|
+
*/
|
|
86
|
+
social: ${JSON.stringify(providers).replace(/","/g, '", "')} as SocialProvider[],
|
|
87
|
+
/** Refuse password sign-in until the email address is verified. */
|
|
88
|
+
requireEmailVerification: false,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type SocialProvider = "google" | "github" | "apple" | "microsoft";
|
|
92
|
+
|
|
93
|
+
/** Whether any second factor can be set up. */
|
|
94
|
+
export const twoFactorAvailable = authConfig.twoFactor.authenticator || authConfig.twoFactor.email;
|
|
95
|
+
`;
|
|
45
96
|
}
|
|
46
97
|
function devVarsEntries(providers) {
|
|
47
98
|
return providers.map((provider) => `${AUTH_PROVIDERS[provider].envPrefix}_CLIENT_ID=
|
|
@@ -50,21 +101,39 @@ ${AUTH_PROVIDERS[provider].envPrefix}_CLIENT_SECRET=
|
|
|
50
101
|
}
|
|
51
102
|
function devVarsExampleEntries(providers) {
|
|
52
103
|
return providers.map((provider) => {
|
|
53
|
-
const { envPrefix, console: console2 } = AUTH_PROVIDERS[provider];
|
|
104
|
+
const { label, envPrefix, console: console2, extra } = AUTH_PROVIDERS[provider];
|
|
54
105
|
return [
|
|
55
106
|
"",
|
|
56
|
-
`# ${
|
|
107
|
+
`# ${label} sign-in: ${console2}`,
|
|
57
108
|
`# Callback URL: <your app URL>/api/auth/callback/${provider}`,
|
|
58
109
|
`${envPrefix}_CLIENT_ID=`,
|
|
59
110
|
`${envPrefix}_CLIENT_SECRET=`,
|
|
111
|
+
...extra,
|
|
60
112
|
""
|
|
61
113
|
].join("\n");
|
|
62
114
|
}).join("");
|
|
63
115
|
}
|
|
64
116
|
|
|
117
|
+
// src/themes.ts
|
|
118
|
+
var THEMES = {
|
|
119
|
+
default: "Calm and centred, indigo accents",
|
|
120
|
+
coral: "Warm and rounded, sign-in as a card over the page",
|
|
121
|
+
amber: "Plain and direct, boxed forms with pill buttons",
|
|
122
|
+
sky: "Crisp blue, bold headings, social sign-in first",
|
|
123
|
+
mono: "Black and white on a fine grid",
|
|
124
|
+
emerald: "Fresh green, sign-in beside a customer quote"
|
|
125
|
+
};
|
|
126
|
+
function parseTheme(value) {
|
|
127
|
+
if (value === void 0) return "default";
|
|
128
|
+
const name = value.trim().toLowerCase();
|
|
129
|
+
if (!(name in THEMES)) throw new Error(`Unknown theme "${value}". Themes: ${Object.keys(THEMES).join(", ")}.`);
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
132
|
+
|
|
65
133
|
// src/versions.ts
|
|
66
134
|
var APP_DEPENDENCIES = {
|
|
67
135
|
"@better-auth/drizzle-adapter": "1.7.5",
|
|
136
|
+
"@better-auth/passkey": "1.7.5",
|
|
68
137
|
// Server components import Slot from here rather than the "radix-ui" umbrella (see components/ui/button.tsx).
|
|
69
138
|
"@radix-ui/react-slot": "1.3.3",
|
|
70
139
|
"@vinext/cloudflare": "1.0.0-beta.8",
|
|
@@ -80,6 +149,14 @@ var APP_DEPENDENCIES = {
|
|
|
80
149
|
sonner: "^2.0.8",
|
|
81
150
|
"tailwind-merge": "^3.7.0",
|
|
82
151
|
"tw-animate-css": "^1.4.0",
|
|
152
|
+
// Typefaces for the six themes (app/globals.css).
|
|
153
|
+
"@fontsource-variable/figtree": "5.3.0",
|
|
154
|
+
"@fontsource-variable/geist": "5.3.0",
|
|
155
|
+
"@fontsource-variable/inter": "5.3.0",
|
|
156
|
+
"@fontsource-variable/noto-sans": "5.3.0",
|
|
157
|
+
"@fontsource-variable/plus-jakarta-sans": "5.3.0",
|
|
158
|
+
// QR codes for authenticator-app (2FA) setup.
|
|
159
|
+
uqr: "0.1.3",
|
|
83
160
|
"drizzle-orm": "^0.45.2",
|
|
84
161
|
react: "19.3.0",
|
|
85
162
|
"react-dom": "19.3.0",
|
|
@@ -194,14 +271,17 @@ function createApp(target, options2 = {}) {
|
|
|
194
271
|
}
|
|
195
272
|
const packageManager = options2.pm ?? detectPackageManager();
|
|
196
273
|
const authProviders = parseAuthProviders(options2.authProviders);
|
|
274
|
+
const authMethods = parseAuthMethods(options2.auth);
|
|
275
|
+
const theme = parseTheme(options2.theme);
|
|
197
276
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
198
277
|
const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
199
278
|
copyTemplate(join2(templatesDir, "app"), dir, {
|
|
200
279
|
APP_NAME: name,
|
|
201
280
|
COMPAT_DATE: compatibilityDate,
|
|
202
281
|
PM: packageManager,
|
|
203
|
-
|
|
282
|
+
THEME: theme
|
|
204
283
|
});
|
|
284
|
+
writeFileSync2(join2(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
205
285
|
appendFileSync(join2(dir, ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
206
286
|
writeJson(join2(dir, "package.json"), {
|
|
207
287
|
name,
|
|
@@ -274,9 +354,10 @@ import { dirname as dirname6, join as join11 } from "path";
|
|
|
274
354
|
import pc6 from "picocolors";
|
|
275
355
|
|
|
276
356
|
// src/generator/grammar.ts
|
|
277
|
-
import { camelCase, FILE_CATEGORIES } from "@flaredev/core";
|
|
357
|
+
import { camelCase, FILE_CATEGORIES, STRING_FORMATS } from "@flaredev/core";
|
|
278
358
|
var SIMPLE_KINDS = ["string", "text", "int", "float", "boolean", "date", "datetime"];
|
|
279
|
-
var
|
|
359
|
+
var FORMAT_ALIASES = { phone: "tel" };
|
|
360
|
+
var ALL_KINDS = [...SIMPLE_KINDS, ...STRING_FORMATS, "phone", "enum", "select", "radio", "multiselect", "file", "belongsTo", "hasMany"];
|
|
280
361
|
var FieldGrammarError = class extends Error {
|
|
281
362
|
constructor(message) {
|
|
282
363
|
super(message);
|
|
@@ -344,15 +425,21 @@ function parseField(spec) {
|
|
|
344
425
|
let match;
|
|
345
426
|
if (SIMPLE_KINDS.includes(type)) {
|
|
346
427
|
field.kind = type;
|
|
347
|
-
} else if (
|
|
348
|
-
field.kind = "
|
|
349
|
-
|
|
428
|
+
} else if (STRING_FORMATS.includes(type) || type in FORMAT_ALIASES) {
|
|
429
|
+
field.kind = "string";
|
|
430
|
+
field.format = FORMAT_ALIASES[type] ?? type;
|
|
431
|
+
} else if (match = /^(enum|select|radio|multiselect)\((.*)\)$/.exec(type)) {
|
|
432
|
+
const choice = match[1];
|
|
433
|
+
field.kind = choice === "multiselect" ? "multiselect" : "enum";
|
|
434
|
+
if (choice === "radio") field.widget = "radio";
|
|
435
|
+
if (choice === "multiselect" && unique) throw new FieldGrammarError(`"${spec}": multiselect fields can't be unique.`);
|
|
436
|
+
const options2 = list(match[2], `${choice} option`, spec);
|
|
350
437
|
for (const option of options2) {
|
|
351
438
|
if (!/^[A-Za-z0-9_-]+$/.test(option)) {
|
|
352
|
-
throw new FieldGrammarError(`"${spec}":
|
|
439
|
+
throw new FieldGrammarError(`"${spec}": ${choice} option "${option}" may only use letters, digits, _ and -.`);
|
|
353
440
|
}
|
|
354
441
|
}
|
|
355
|
-
if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate
|
|
442
|
+
if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
|
|
356
443
|
field.options = options2;
|
|
357
444
|
} else if (match = /^file(?::\[(.*)\])?$/.exec(type)) {
|
|
358
445
|
field.kind = "file";
|
|
@@ -387,7 +474,7 @@ function parseField(spec) {
|
|
|
387
474
|
const base = type.replace(/[([].*$/, "").replace(/:$/, "");
|
|
388
475
|
throw new FieldGrammarError(`"${spec}": unknown type "${type}".${suggest(base, ALL_KINDS)} Types: ${ALL_KINDS.join(", ")}.`);
|
|
389
476
|
}
|
|
390
|
-
if (field.kind === "string") {
|
|
477
|
+
if (field.kind === "string" && !field.format) {
|
|
391
478
|
if (/^email$|Email$/.test(key)) field.format = "email";
|
|
392
479
|
else if (/^(url|website)$|(Url|Website)$/.test(key)) field.format = "url";
|
|
393
480
|
}
|
|
@@ -505,6 +592,7 @@ function options(field) {
|
|
|
505
592
|
if (!field.required) opts.required = false;
|
|
506
593
|
if (field.unique) opts.unique = true;
|
|
507
594
|
if (field.format) opts.format = field.format;
|
|
595
|
+
if (field.widget) opts.widget = field.widget;
|
|
508
596
|
if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
|
|
509
597
|
return opts;
|
|
510
598
|
}
|
|
@@ -513,6 +601,8 @@ function toField(field) {
|
|
|
513
601
|
switch (field.kind) {
|
|
514
602
|
case "enum":
|
|
515
603
|
return builders.enum(field.options, opts);
|
|
604
|
+
case "multiselect":
|
|
605
|
+
return builders.multiselect(field.options, opts);
|
|
516
606
|
case "file":
|
|
517
607
|
return builders.file(field.accept, opts);
|
|
518
608
|
case "belongsTo":
|
|
@@ -534,6 +624,8 @@ function renderField(field) {
|
|
|
534
624
|
switch (field.kind) {
|
|
535
625
|
case "enum":
|
|
536
626
|
return `${field.key}: field.enum(${args(literal(field.options), opts)}),`;
|
|
627
|
+
case "multiselect":
|
|
628
|
+
return `${field.key}: field.multiselect(${args(literal(field.options), opts)}),`;
|
|
537
629
|
case "file":
|
|
538
630
|
return `${field.key}: field.file(${args(literal(field.accept), opts)}),`;
|
|
539
631
|
case "belongsTo":
|
|
@@ -1843,6 +1935,9 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1843
1935
|
case "enum":
|
|
1844
1936
|
expr = `text(${col}, { enum: ${q(def.options)} })`;
|
|
1845
1937
|
break;
|
|
1938
|
+
case "multiselect":
|
|
1939
|
+
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q(option)).join(" | ")})[]>()`;
|
|
1940
|
+
break;
|
|
1846
1941
|
case "belongsTo": {
|
|
1847
1942
|
const target = resources.get(def.target);
|
|
1848
1943
|
if (!target) {
|
|
@@ -1856,7 +1951,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1856
1951
|
expr = `text(${col})`;
|
|
1857
1952
|
}
|
|
1858
1953
|
if (def.required) expr += ".notNull()";
|
|
1859
|
-
if (def.unique) expr += ".unique()";
|
|
1954
|
+
if ("unique" in def && def.unique) expr += ".unique()";
|
|
1860
1955
|
if ("default" in def && def.default !== void 0) expr += `.default(${q(def.default)})`;
|
|
1861
1956
|
return expr;
|
|
1862
1957
|
}
|
|
@@ -2894,11 +2989,83 @@ async function genSecurity(options2 = {}) {
|
|
|
2894
2989
|
return { files, migrations, bindings };
|
|
2895
2990
|
}
|
|
2896
2991
|
|
|
2992
|
+
// src/commands/theme.ts
|
|
2993
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
2994
|
+
import { join as join13 } from "path";
|
|
2995
|
+
import pc8 from "picocolors";
|
|
2996
|
+
var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
|
|
2997
|
+
function setTheme(name, cwd = process.cwd(), log = console.log) {
|
|
2998
|
+
const appRoot = findAppRoot(cwd);
|
|
2999
|
+
const path = join13(appRoot, "lib/site.ts");
|
|
3000
|
+
if (!existsSync14(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
|
|
3001
|
+
const source = readFileSync10(path, "utf8");
|
|
3002
|
+
const current = THEME_LINE.exec(source)?.[1];
|
|
3003
|
+
if (!name) {
|
|
3004
|
+
for (const [theme2, description] of Object.entries(THEMES)) {
|
|
3005
|
+
log(`${theme2 === current ? pc8.green("\u25CF") : " "} ${theme2.padEnd(8)} ${pc8.dim(description)}`);
|
|
3006
|
+
}
|
|
3007
|
+
log(pc8.dim("\nSwitch with `flare theme <name>`. FLARE_THEME in .env overrides it for a build."));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const theme = parseTheme(name);
|
|
3011
|
+
if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
|
|
3012
|
+
writeFileSync8(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
|
|
3013
|
+
log(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// src/commands/create-prompts.ts
|
|
3017
|
+
import * as prompts from "@clack/prompts";
|
|
3018
|
+
var canPrompt = (yes) => !yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3019
|
+
function stopIfCancelled(value) {
|
|
3020
|
+
if (prompts.isCancel(value)) {
|
|
3021
|
+
prompts.cancel("Cancelled: nothing was created.");
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
}
|
|
3024
|
+
return value;
|
|
3025
|
+
}
|
|
3026
|
+
async function askCreateQuestions(given) {
|
|
3027
|
+
prompts.intro("Let's set up your Flare app");
|
|
3028
|
+
const answers = { ...given };
|
|
3029
|
+
if (given.theme === void 0) {
|
|
3030
|
+
answers.theme = stopIfCancelled(
|
|
3031
|
+
await prompts.select({
|
|
3032
|
+
message: "Pick a look (change it later in lib/site.ts or with FLARE_THEME)",
|
|
3033
|
+
initialValue: "default",
|
|
3034
|
+
options: Object.entries(THEMES).map(([value, hint]) => ({ value, label: value, hint }))
|
|
3035
|
+
})
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
if (given.auth === void 0) {
|
|
3039
|
+
const methods = stopIfCancelled(
|
|
3040
|
+
await prompts.multiselect({
|
|
3041
|
+
message: "How can people sign in? Email and password is always on. (space to toggle)",
|
|
3042
|
+
initialValues: DEFAULT_AUTH_METHODS,
|
|
3043
|
+
required: false,
|
|
3044
|
+
options: Object.entries(AUTH_METHODS).map(([value, label]) => ({ value, label }))
|
|
3045
|
+
})
|
|
3046
|
+
);
|
|
3047
|
+
answers.auth = methods.length ? methods.join(",") : "none";
|
|
3048
|
+
}
|
|
3049
|
+
if (given.authProviders === void 0) {
|
|
3050
|
+
const providers = stopIfCancelled(
|
|
3051
|
+
await prompts.multiselect({
|
|
3052
|
+
message: "Social sign-in? Each needs its own credentials later. (space to toggle, enter to skip)",
|
|
3053
|
+
initialValues: [],
|
|
3054
|
+
required: false,
|
|
3055
|
+
options: Object.entries(AUTH_PROVIDERS).map(([value, { label }]) => ({ value, label }))
|
|
3056
|
+
})
|
|
3057
|
+
);
|
|
3058
|
+
answers.authProviders = providers.join(",");
|
|
3059
|
+
}
|
|
3060
|
+
prompts.outro("Creating your app");
|
|
3061
|
+
return answers;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
2897
3064
|
// src/commands/gen.ts
|
|
2898
|
-
import { existsSync as
|
|
2899
|
-
import { dirname as dirname8, join as
|
|
3065
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
3066
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
2900
3067
|
import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
|
|
2901
|
-
import
|
|
3068
|
+
import pc9 from "picocolors";
|
|
2902
3069
|
function validateFields(name, fields, others, log) {
|
|
2903
3070
|
const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
|
|
2904
3071
|
for (const field of fields) {
|
|
@@ -2909,7 +3076,7 @@ function validateFields(name, fields, others, log) {
|
|
|
2909
3076
|
const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
|
|
2910
3077
|
const graph = relationGraph2([...others, candidate]);
|
|
2911
3078
|
for (const pending of graph.pending.filter((p) => p.resource === name)) {
|
|
2912
|
-
log(
|
|
3079
|
+
log(pc9.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
|
|
2913
3080
|
}
|
|
2914
3081
|
}
|
|
2915
3082
|
async function genResource(rawName, options2) {
|
|
@@ -2917,8 +3084,8 @@ async function genResource(rawName, options2) {
|
|
|
2917
3084
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
2918
3085
|
const name = resourceName(rawName);
|
|
2919
3086
|
const relativePath = descriptorPath(name);
|
|
2920
|
-
const path =
|
|
2921
|
-
const exists =
|
|
3087
|
+
const path = join14(appRoot, relativePath);
|
|
3088
|
+
const exists = existsSync15(path);
|
|
2922
3089
|
if (!exists && !options2.fields) {
|
|
2923
3090
|
throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
|
|
2924
3091
|
}
|
|
@@ -2929,12 +3096,12 @@ async function genResource(rawName, options2) {
|
|
|
2929
3096
|
validateFields(name, fields, others, log);
|
|
2930
3097
|
if (!exists) {
|
|
2931
3098
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
2932
|
-
|
|
2933
|
-
log(`${
|
|
3099
|
+
writeFileSync9(path, renderDescriptor(name, fields));
|
|
3100
|
+
log(`${pc9.green("create".padEnd(9))} ${relativePath}`);
|
|
2934
3101
|
} else {
|
|
2935
3102
|
try {
|
|
2936
3103
|
const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
|
|
2937
|
-
log(`${(outcome === "identical" ?
|
|
3104
|
+
log(`${(outcome === "identical" ? pc9.dim : pc9.yellow)(outcome.padEnd(9))} ${relativePath}`);
|
|
2938
3105
|
} catch (error) {
|
|
2939
3106
|
if (error instanceof DriftError) {
|
|
2940
3107
|
throw new Error(
|
|
@@ -2960,28 +3127,28 @@ async function genResource(rawName, options2) {
|
|
|
2960
3127
|
}
|
|
2961
3128
|
const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
|
|
2962
3129
|
if (skipped.length) {
|
|
2963
|
-
log(
|
|
3130
|
+
log(pc9.yellow(`
|
|
2964
3131
|
Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
|
|
2965
3132
|
}
|
|
2966
3133
|
let migrations = [];
|
|
2967
3134
|
if (!options2.skipMigration) {
|
|
2968
3135
|
const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
|
|
2969
3136
|
migrations = result.files;
|
|
2970
|
-
for (const file of migrations) log(`${
|
|
2971
|
-
for (const repair of result.repairs) log(
|
|
3137
|
+
for (const file of migrations) log(`${pc9.green("create".padEnd(9))} ${file}`);
|
|
3138
|
+
for (const repair of result.repairs) log(pc9.dim(`repaired ${repair}`));
|
|
2972
3139
|
if (migrations.length) log(`
|
|
2973
|
-
Next: ${
|
|
2974
|
-
else if (exists) log(
|
|
3140
|
+
Next: ${pc9.bold("flare migrate")} to apply it locally.`);
|
|
3141
|
+
else if (exists) log(pc9.dim("\nNo table changes, so no migration."));
|
|
2975
3142
|
}
|
|
2976
3143
|
const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
|
|
2977
3144
|
return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
|
|
2978
3145
|
}
|
|
2979
3146
|
|
|
2980
3147
|
// src/commands/gen-migration.ts
|
|
2981
|
-
import { existsSync as
|
|
2982
|
-
import { basename as basename2, join as
|
|
3148
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
3149
|
+
import { basename as basename2, join as join15 } from "path";
|
|
2983
3150
|
import { snakeCase } from "@flaredev/core";
|
|
2984
|
-
import
|
|
3151
|
+
import pc10 from "picocolors";
|
|
2985
3152
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
2986
3153
|
-- Write SQL that undoes the up migration, e.g.:
|
|
2987
3154
|
-- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
|
|
@@ -2997,31 +3164,31 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
2997
3164
|
log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
|
|
2998
3165
|
return [];
|
|
2999
3166
|
}
|
|
3000
|
-
for (const file of files) log(`${
|
|
3001
|
-
for (const repair of repairs) log(
|
|
3167
|
+
for (const file of files) log(`${pc10.green("create".padEnd(9))} ${file}`);
|
|
3168
|
+
for (const repair of repairs) log(pc10.dim(`repaired ${repair}`));
|
|
3002
3169
|
if (!options2.fromSchema) {
|
|
3003
|
-
const downDir =
|
|
3170
|
+
const downDir = join15(appRoot, "migrations", "down");
|
|
3004
3171
|
mkdirSync8(downDir, { recursive: true });
|
|
3005
3172
|
for (const file of files) {
|
|
3006
|
-
const down =
|
|
3007
|
-
if (
|
|
3008
|
-
|
|
3009
|
-
log(`${
|
|
3173
|
+
const down = join15(downDir, basename2(file));
|
|
3174
|
+
if (existsSync16(down)) continue;
|
|
3175
|
+
writeFileSync10(down, DOWN_TEMPLATE(basename2(file)));
|
|
3176
|
+
log(`${pc10.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3010
3177
|
}
|
|
3011
3178
|
log(`
|
|
3012
|
-
Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${
|
|
3179
|
+
Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc10.bold("flare migrate")}.`);
|
|
3013
3180
|
} else {
|
|
3014
3181
|
log(`
|
|
3015
|
-
Next: ${
|
|
3182
|
+
Next: ${pc10.bold("flare migrate")}.`);
|
|
3016
3183
|
}
|
|
3017
3184
|
return files;
|
|
3018
3185
|
}
|
|
3019
3186
|
|
|
3020
3187
|
// src/commands/gen-policy.ts
|
|
3021
|
-
import { existsSync as
|
|
3022
|
-
import { dirname as dirname9, join as
|
|
3188
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
3189
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
3023
3190
|
import { definePolicy } from "@flaredev/core";
|
|
3024
|
-
import
|
|
3191
|
+
import pc11 from "picocolors";
|
|
3025
3192
|
function parseRoles(input, what = "--roles") {
|
|
3026
3193
|
const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
|
|
3027
3194
|
if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
|
|
@@ -3037,8 +3204,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3037
3204
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3038
3205
|
}
|
|
3039
3206
|
const relative2 = policyPath(name);
|
|
3040
|
-
const path =
|
|
3041
|
-
const exists =
|
|
3207
|
+
const path = join16(appRoot, relative2);
|
|
3208
|
+
const exists = existsSync17(path);
|
|
3042
3209
|
let roles;
|
|
3043
3210
|
let deleteRoles;
|
|
3044
3211
|
if (options2.roles || !exists) {
|
|
@@ -3050,12 +3217,12 @@ async function genPolicy(rawName, options2) {
|
|
|
3050
3217
|
const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
|
|
3051
3218
|
if (!exists) {
|
|
3052
3219
|
mkdirSync9(dirname9(path), { recursive: true });
|
|
3053
|
-
|
|
3054
|
-
log(`${
|
|
3220
|
+
writeFileSync11(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3221
|
+
log(`${pc11.green("create".padEnd(9))} ${relative2}`);
|
|
3055
3222
|
} else {
|
|
3056
3223
|
try {
|
|
3057
3224
|
const outcome = writeGenerated(path, block, { force: options2.force });
|
|
3058
|
-
log(`${(outcome === "identical" ?
|
|
3225
|
+
log(`${(outcome === "identical" ? pc11.dim : pc11.yellow)(outcome.padEnd(9))} ${relative2}`);
|
|
3059
3226
|
} catch (error) {
|
|
3060
3227
|
if (error instanceof DriftError) {
|
|
3061
3228
|
throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
|
|
@@ -3067,15 +3234,15 @@ async function genPolicy(rawName, options2) {
|
|
|
3067
3234
|
const policies = await loadPolicies(appRoot);
|
|
3068
3235
|
logResults(applyPlan(appRoot, planFiles(resources, policies)), log);
|
|
3069
3236
|
log(`
|
|
3070
|
-
${
|
|
3237
|
+
${pc11.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3071
3238
|
return { name, path: relative2 };
|
|
3072
3239
|
}
|
|
3073
3240
|
|
|
3074
3241
|
// src/commands/migrate.ts
|
|
3075
|
-
import { existsSync as
|
|
3242
|
+
import { existsSync as existsSync18, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
|
|
3076
3243
|
import { tmpdir } from "os";
|
|
3077
|
-
import { join as
|
|
3078
|
-
import
|
|
3244
|
+
import { join as join17 } from "path";
|
|
3245
|
+
import pc12 from "picocolors";
|
|
3079
3246
|
|
|
3080
3247
|
// src/generator/down.ts
|
|
3081
3248
|
var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
|
|
@@ -3192,7 +3359,7 @@ async function rollback(options2 = {}) {
|
|
|
3192
3359
|
const steps = options2.steps ?? 1;
|
|
3193
3360
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3194
3361
|
const { appRoot, db, target, where, wrangler, log } = context(options2);
|
|
3195
|
-
const migrationsDir =
|
|
3362
|
+
const migrationsDir = join17(appRoot, db.migrations_dir ?? "migrations");
|
|
3196
3363
|
const applied = await appliedMigrations(options2, steps);
|
|
3197
3364
|
if (applied.length === 0) {
|
|
3198
3365
|
log("Nothing to roll back.");
|
|
@@ -3201,18 +3368,18 @@ async function rollback(options2 = {}) {
|
|
|
3201
3368
|
const plan = [];
|
|
3202
3369
|
for (const { name } of applied) {
|
|
3203
3370
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3204
|
-
const handWritten =
|
|
3205
|
-
if (
|
|
3206
|
-
const sql2 =
|
|
3371
|
+
const handWritten = join17(migrationsDir, "down", name);
|
|
3372
|
+
if (existsSync18(handWritten)) {
|
|
3373
|
+
const sql2 = readFileSync11(handWritten, "utf8");
|
|
3207
3374
|
if (splitStatements(sql2).length === 0) {
|
|
3208
3375
|
throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
|
|
3209
3376
|
}
|
|
3210
3377
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3211
3378
|
continue;
|
|
3212
3379
|
}
|
|
3213
|
-
const upPath =
|
|
3214
|
-
if (!
|
|
3215
|
-
const down = deriveDown(
|
|
3380
|
+
const upPath = join17(migrationsDir, name);
|
|
3381
|
+
if (!existsSync18(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3382
|
+
const down = deriveDown(readFileSync11(upPath, "utf8"));
|
|
3216
3383
|
if (!down.reversible) {
|
|
3217
3384
|
throw new Error(
|
|
3218
3385
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -3221,37 +3388,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3221
3388
|
}
|
|
3222
3389
|
plan.push({ name, sql: down.sql, source: "derived" });
|
|
3223
3390
|
}
|
|
3224
|
-
log(`${
|
|
3391
|
+
log(`${pc12.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
|
|
3225
3392
|
for (const step of plan) {
|
|
3226
|
-
log(` ${step.name} ${
|
|
3227
|
-
for (const line of step.sql.trim().split("\n")) log(
|
|
3393
|
+
log(` ${step.name} ${pc12.dim(`(${step.source})`)}`);
|
|
3394
|
+
for (const line of step.sql.trim().split("\n")) log(pc12.dim(` ${line}`));
|
|
3228
3395
|
}
|
|
3229
3396
|
if (options2.remote && !options2.yes) {
|
|
3230
|
-
log(
|
|
3397
|
+
log(pc12.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
|
|
3231
3398
|
return 1;
|
|
3232
3399
|
}
|
|
3233
3400
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3234
3401
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3235
|
-
const dir = mkdtempSync(
|
|
3402
|
+
const dir = mkdtempSync(join17(tmpdir(), "flare-rollback-"));
|
|
3236
3403
|
try {
|
|
3237
|
-
const file =
|
|
3238
|
-
|
|
3404
|
+
const file = join17(dir, "rollback.sql");
|
|
3405
|
+
writeFileSync12(file, script);
|
|
3239
3406
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3240
3407
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
3241
3408
|
${result.output.trim()}`);
|
|
3242
3409
|
} finally {
|
|
3243
3410
|
rmSync2(dir, { recursive: true, force: true });
|
|
3244
3411
|
}
|
|
3245
|
-
log(
|
|
3412
|
+
log(pc12.green(`
|
|
3246
3413
|
Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
3247
3414
|
return 0;
|
|
3248
3415
|
}
|
|
3249
3416
|
|
|
3250
3417
|
// src/commands/rm.ts
|
|
3251
|
-
import { existsSync as
|
|
3252
|
-
import { dirname as dirname10, join as
|
|
3418
|
+
import { existsSync as existsSync19, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3419
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
3253
3420
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
3254
|
-
import
|
|
3421
|
+
import pc13 from "picocolors";
|
|
3255
3422
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
3256
3423
|
function handWrittenReason(source, header, options2 = {}) {
|
|
3257
3424
|
const parts = splitMarkers(source);
|
|
@@ -3268,10 +3435,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
3268
3435
|
return null;
|
|
3269
3436
|
}
|
|
3270
3437
|
function removeEmptyDirs(appRoot, start) {
|
|
3271
|
-
let dir = dirname10(
|
|
3272
|
-
const roots = [
|
|
3438
|
+
let dir = dirname10(join18(appRoot, start));
|
|
3439
|
+
const roots = [join18(appRoot, "app", "api"), join18(appRoot, "app", "admin")];
|
|
3273
3440
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
3274
|
-
while (inside() &&
|
|
3441
|
+
while (inside() && existsSync19(dir) && readdirSync7(dir).length === 0) {
|
|
3275
3442
|
rmdirSync(dir);
|
|
3276
3443
|
dir = dirname10(dir);
|
|
3277
3444
|
}
|
|
@@ -3300,7 +3467,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3300
3467
|
if (entry) {
|
|
3301
3468
|
owned.add(entry.file);
|
|
3302
3469
|
for (const file of planFiles(all)) {
|
|
3303
|
-
if (
|
|
3470
|
+
if (existsSync19(join18(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
3304
3471
|
}
|
|
3305
3472
|
}
|
|
3306
3473
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -3309,7 +3476,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3309
3476
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
3310
3477
|
const findings = [];
|
|
3311
3478
|
for (const path of owned) {
|
|
3312
|
-
const source =
|
|
3479
|
+
const source = readFileSync12(join18(appRoot, path), "utf8");
|
|
3313
3480
|
let wrapper;
|
|
3314
3481
|
if (entry && path === entry.file) {
|
|
3315
3482
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -3330,34 +3497,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
3330
3497
|
}
|
|
3331
3498
|
const removed = [...owned].sort();
|
|
3332
3499
|
for (const path of removed) {
|
|
3333
|
-
unlinkSync2(
|
|
3500
|
+
unlinkSync2(join18(appRoot, path));
|
|
3334
3501
|
removeEmptyDirs(appRoot, path);
|
|
3335
|
-
log(`${
|
|
3502
|
+
log(`${pc13.red("remove".padEnd(9))} ${path}`);
|
|
3336
3503
|
}
|
|
3337
3504
|
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
|
|
3338
3505
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
3339
|
-
if (table &&
|
|
3340
|
-
const seeds = readdirSync7(
|
|
3341
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
3506
|
+
if (table && existsSync19(join18(appRoot, "seeds"))) {
|
|
3507
|
+
const seeds = readdirSync7(join18(appRoot, "seeds")).filter(
|
|
3508
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join18(appRoot, "seeds", file), "utf8"))
|
|
3342
3509
|
);
|
|
3343
|
-
if (seeds.length) log(
|
|
3510
|
+
if (seeds.length) log(pc13.yellow(`
|
|
3344
3511
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
3345
3512
|
}
|
|
3346
3513
|
let migrations = [];
|
|
3347
3514
|
if (!options2.skipMigration && entry) {
|
|
3348
3515
|
const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
|
|
3349
3516
|
migrations = result.files;
|
|
3350
|
-
for (const file of migrations) log(`${
|
|
3351
|
-
for (const repair of result.repairs) log(
|
|
3517
|
+
for (const file of migrations) log(`${pc13.green("create".padEnd(9))} ${file}`);
|
|
3518
|
+
for (const repair of result.repairs) log(pc13.dim(`repaired ${repair}`));
|
|
3352
3519
|
if (migrations.length) log(`
|
|
3353
|
-
Next: ${
|
|
3520
|
+
Next: ${pc13.bold("flare migrate")} to drop the table locally.`);
|
|
3354
3521
|
}
|
|
3355
3522
|
return { removed, migrations };
|
|
3356
3523
|
}
|
|
3357
3524
|
|
|
3358
3525
|
// src/commands/role.ts
|
|
3359
3526
|
import { humanize } from "@flaredev/core";
|
|
3360
|
-
import
|
|
3527
|
+
import pc14 from "picocolors";
|
|
3361
3528
|
var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
3362
3529
|
function roleInsertSql(name, label) {
|
|
3363
3530
|
if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
|
|
@@ -3402,24 +3569,24 @@ ${result.output.trim()}`);
|
|
|
3402
3569
|
}
|
|
3403
3570
|
const [first] = extractJson(result.output);
|
|
3404
3571
|
const added = first?.results?.[0];
|
|
3405
|
-
log(
|
|
3406
|
-
log(
|
|
3572
|
+
log(pc14.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
|
|
3573
|
+
log(pc14.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
|
|
3407
3574
|
return 0;
|
|
3408
3575
|
}
|
|
3409
3576
|
|
|
3410
3577
|
// src/commands/seed.ts
|
|
3411
|
-
import { existsSync as
|
|
3578
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
3412
3579
|
import { createRequire } from "module";
|
|
3413
|
-
import { join as
|
|
3580
|
+
import { join as join19 } from "path";
|
|
3414
3581
|
import { pathToFileURL } from "url";
|
|
3415
3582
|
import { kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
|
|
3416
3583
|
import { createJiti as createJiti4 } from "jiti";
|
|
3417
|
-
import
|
|
3584
|
+
import pc15 from "picocolors";
|
|
3418
3585
|
var SEEDS_DIR = "seeds";
|
|
3419
3586
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
3420
3587
|
function seedFiles(appRoot, names) {
|
|
3421
|
-
const dir =
|
|
3422
|
-
const all =
|
|
3588
|
+
const dir = join19(appRoot, SEEDS_DIR);
|
|
3589
|
+
const all = existsSync20(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
3423
3590
|
if (!names?.length) return all;
|
|
3424
3591
|
return names.map((name) => {
|
|
3425
3592
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -3434,29 +3601,29 @@ async function runSeeds(options2 = {}) {
|
|
|
3434
3601
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3435
3602
|
const files = seedFiles(appRoot, options2.names);
|
|
3436
3603
|
if (files.length === 0) {
|
|
3437
|
-
log(`No seeds found. Create one with ${
|
|
3604
|
+
log(`No seeds found. Create one with ${pc15.bold("flare seed:make <name>")}.`);
|
|
3438
3605
|
return;
|
|
3439
3606
|
}
|
|
3440
|
-
const jiti = createJiti4(
|
|
3441
|
-
const appRequire = createRequire(
|
|
3607
|
+
const jiti = createJiti4(join19(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
3608
|
+
const appRequire = createRequire(join19(appRoot, "package.json"));
|
|
3442
3609
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
3443
3610
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
3444
|
-
const schema = await jiti.import(
|
|
3611
|
+
const schema = await jiti.import(join19(appRoot, "db/schema.ts"));
|
|
3445
3612
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3446
|
-
configPath:
|
|
3447
|
-
persist: { path:
|
|
3613
|
+
configPath: join19(appRoot, "wrangler.jsonc"),
|
|
3614
|
+
persist: { path: join19(appRoot, LOCAL_STATE) }
|
|
3448
3615
|
});
|
|
3449
3616
|
try {
|
|
3450
3617
|
const bindings = env;
|
|
3451
3618
|
if (!bindings.DB) throw new Error("No DB binding found in wrangler.jsonc.");
|
|
3452
3619
|
const db = drizzle(bindings.DB, { schema });
|
|
3453
3620
|
for (const file of files) {
|
|
3454
|
-
const mod = await jiti.import(
|
|
3621
|
+
const mod = await jiti.import(join19(appRoot, SEEDS_DIR, file));
|
|
3455
3622
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
3456
|
-
log(`${
|
|
3623
|
+
log(`${pc15.cyan("seed")} ${file}`);
|
|
3457
3624
|
await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
|
|
3458
3625
|
}
|
|
3459
|
-
log(
|
|
3626
|
+
log(pc15.green(`
|
|
3460
3627
|
Ran ${files.length} seed(s) against the local database.`));
|
|
3461
3628
|
} finally {
|
|
3462
3629
|
await dispose();
|
|
@@ -3467,6 +3634,11 @@ function sampleValue(key, def, row) {
|
|
|
3467
3634
|
case "string":
|
|
3468
3635
|
if (def.format === "email") return `\`${kebabCase3(key).replace(/-?email$/, "") || "user"}\${i}@example.com\``;
|
|
3469
3636
|
if (def.format === "url") return `\`https://example.com/\${i}\``;
|
|
3637
|
+
if (def.format === "tel") return `\`+1202555\${String(1000 + i).slice(-4)}\``;
|
|
3638
|
+
if (def.format === "domain") return `\`example\${i}.com\``;
|
|
3639
|
+
if (def.format === "country") return JSON.stringify(["US", "GB", "UG", "KE", "DE"][row % 5]);
|
|
3640
|
+
if (def.format === "color") return JSON.stringify("#f2541d");
|
|
3641
|
+
if (def.format === "slug") return `\`${kebabCase3(def.label ?? key)}-\${i}\``;
|
|
3470
3642
|
return `\`${def.label} \${i}\``;
|
|
3471
3643
|
case "text":
|
|
3472
3644
|
return `"Lorem ipsum dolor sit amet."`;
|
|
@@ -3482,6 +3654,8 @@ function sampleValue(key, def, row) {
|
|
|
3482
3654
|
return "new Date().toISOString()";
|
|
3483
3655
|
case "enum":
|
|
3484
3656
|
return JSON.stringify(def.options[row % def.options.length]);
|
|
3657
|
+
case "multiselect":
|
|
3658
|
+
return JSON.stringify(def.options.slice(0, Math.max(1, def.minItems ?? 1)));
|
|
3485
3659
|
case "file":
|
|
3486
3660
|
return void 0;
|
|
3487
3661
|
case "belongsTo":
|
|
@@ -3525,8 +3699,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3525
3699
|
const name = kebabCase3(rawName);
|
|
3526
3700
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
3527
3701
|
const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
3528
|
-
const path =
|
|
3529
|
-
if (
|
|
3702
|
+
const path = join19(appRoot, relative2);
|
|
3703
|
+
if (existsSync20(path)) throw new Error(`${relative2} already exists.`);
|
|
3530
3704
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
3531
3705
|
let resource;
|
|
3532
3706
|
if (options2.resource) {
|
|
@@ -3535,27 +3709,27 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3535
3709
|
} else {
|
|
3536
3710
|
resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
|
|
3537
3711
|
}
|
|
3538
|
-
mkdirSync10(
|
|
3539
|
-
|
|
3540
|
-
log(`${
|
|
3712
|
+
mkdirSync10(join19(appRoot, SEEDS_DIR), { recursive: true });
|
|
3713
|
+
writeFileSync13(path, renderSeed(resource));
|
|
3714
|
+
log(`${pc15.green("create".padEnd(9))} ${relative2}`);
|
|
3541
3715
|
log(`
|
|
3542
|
-
Run it with ${
|
|
3716
|
+
Run it with ${pc15.bold(`flare seed ${name}`)}.`);
|
|
3543
3717
|
return relative2;
|
|
3544
3718
|
}
|
|
3545
3719
|
|
|
3546
3720
|
// src/commands/billing-sync.ts
|
|
3547
3721
|
import { createRequire as createRequire2 } from "module";
|
|
3548
|
-
import { existsSync as
|
|
3722
|
+
import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
3549
3723
|
import { tmpdir as tmpdir2 } from "os";
|
|
3550
|
-
import { join as
|
|
3724
|
+
import { join as join20 } from "path";
|
|
3551
3725
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3552
3726
|
import { kebabCase as kebabCase4 } from "@flaredev/core";
|
|
3553
|
-
import
|
|
3727
|
+
import pc16 from "picocolors";
|
|
3554
3728
|
function stripeKeys(appRoot) {
|
|
3555
3729
|
const map = {};
|
|
3556
|
-
const path =
|
|
3557
|
-
if (
|
|
3558
|
-
for (const line of
|
|
3730
|
+
const path = join20(appRoot, ".dev.vars");
|
|
3731
|
+
if (existsSync21(path)) {
|
|
3732
|
+
for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
|
|
3559
3733
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
3560
3734
|
if (match) map[match[1]] = match[2].trim();
|
|
3561
3735
|
}
|
|
@@ -3638,10 +3812,10 @@ async function syncPlans(options2 = {}) {
|
|
|
3638
3812
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
3639
3813
|
const db = readD1Databases(appRoot)[0];
|
|
3640
3814
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
3641
|
-
const appRequire = createRequire2(
|
|
3815
|
+
const appRequire = createRequire2(join20(appRoot, "package.json"));
|
|
3642
3816
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
3643
3817
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
3644
|
-
const appName = JSON.parse(
|
|
3818
|
+
const appName = JSON.parse(readFileSync13(join20(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
3645
3819
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
3646
3820
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
3647
3821
|
if (products.length === 0) {
|
|
@@ -3662,10 +3836,10 @@ async function syncPlans(options2 = {}) {
|
|
|
3662
3836
|
${existingResult.output.trim()}`);
|
|
3663
3837
|
}
|
|
3664
3838
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
3665
|
-
const dir = mkdtempSync2(
|
|
3839
|
+
const dir = mkdtempSync2(join20(tmpdir2(), "flare-sync-"));
|
|
3666
3840
|
try {
|
|
3667
|
-
const file =
|
|
3668
|
-
|
|
3841
|
+
const file = join20(dir, "sync-plans.sql");
|
|
3842
|
+
writeFileSync14(file, renderSyncSql(rows));
|
|
3669
3843
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3670
3844
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
3671
3845
|
${result.output.trim()}`);
|
|
@@ -3677,26 +3851,26 @@ ${result.output.trim()}`);
|
|
|
3677
3851
|
const isNew = !known.has(row.stripePriceId);
|
|
3678
3852
|
if (isNew) created++;
|
|
3679
3853
|
const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
|
|
3680
|
-
log(`${(isNew ?
|
|
3854
|
+
log(`${(isNew ? pc16.green("create") : pc16.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc16.dim(price)}`);
|
|
3681
3855
|
}
|
|
3682
|
-
for (const name of skipped) log(
|
|
3856
|
+
for (const name of skipped) log(pc16.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
|
|
3683
3857
|
const configuration = await syncPortalConfiguration(stripe, rows, appName);
|
|
3684
|
-
if (configuration) log(`${
|
|
3685
|
-
log(
|
|
3858
|
+
if (configuration) log(`${pc16.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
|
|
3859
|
+
log(pc16.green(`
|
|
3686
3860
|
${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
|
|
3687
3861
|
return { created, updated: rows.length - created, skipped };
|
|
3688
3862
|
}
|
|
3689
3863
|
|
|
3690
3864
|
// src/commands/sync.ts
|
|
3691
|
-
import { existsSync as
|
|
3692
|
-
import { join as
|
|
3693
|
-
import
|
|
3865
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3866
|
+
import { join as join21 } from "path";
|
|
3867
|
+
import pc17 from "picocolors";
|
|
3694
3868
|
async function syncTypes(options2 = {}) {
|
|
3695
3869
|
const log = options2.log ?? ((message) => console.log(message));
|
|
3696
3870
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3697
3871
|
const all = await loadResources(appRoot);
|
|
3698
3872
|
const policies = await loadPolicies(appRoot);
|
|
3699
|
-
const tracksPolicies = policies.length > 0 ||
|
|
3873
|
+
const tracksPolicies = policies.length > 0 || existsSync22(join21(appRoot, "policies/index.ts"));
|
|
3700
3874
|
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
3701
3875
|
if (!options2.check) ensureSupportFiles(appRoot, log);
|
|
3702
3876
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -3708,42 +3882,42 @@ async function syncTypes(options2 = {}) {
|
|
|
3708
3882
|
if (drift.length) {
|
|
3709
3883
|
log(
|
|
3710
3884
|
`
|
|
3711
|
-
${
|
|
3885
|
+
${pc17.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
|
|
3712
3886
|
Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
|
|
3713
3887
|
);
|
|
3714
3888
|
}
|
|
3715
3889
|
if (unmarked.length) {
|
|
3716
3890
|
log(`
|
|
3717
|
-
${
|
|
3891
|
+
${pc17.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
|
|
3718
3892
|
}
|
|
3719
3893
|
if (orphans.length) {
|
|
3720
3894
|
const resources = [...new Set(orphans.map((o) => o.resource))];
|
|
3721
3895
|
log(`
|
|
3722
|
-
${
|
|
3896
|
+
${pc17.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
|
|
3723
3897
|
for (const orphan of orphans) log(` ${orphan.path}`);
|
|
3724
3898
|
log(
|
|
3725
|
-
`If the resource was deleted, remove them with ${
|
|
3899
|
+
`If the resource was deleted, remove them with ${pc17.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
|
|
3726
3900
|
);
|
|
3727
3901
|
}
|
|
3728
3902
|
const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
|
|
3729
3903
|
if (tablesChanged && !options2.check) {
|
|
3730
3904
|
log(`
|
|
3731
|
-
${
|
|
3905
|
+
${pc17.cyan("Tables changed.")} Create a migration with ${pc17.bold("flare gen migration <name> --from-schema")}, then ${pc17.bold("flare migrate")}.`);
|
|
3732
3906
|
}
|
|
3733
3907
|
const problems = drift.length + unmarked.length + orphans.length;
|
|
3734
3908
|
if (options2.check) {
|
|
3735
3909
|
const stale = changed.length;
|
|
3736
|
-
if (stale === 0 && problems === 0) log(
|
|
3910
|
+
if (stale === 0 && problems === 0) log(pc17.green("Generated files are in sync with the resource descriptors."));
|
|
3737
3911
|
else log(`
|
|
3738
|
-
${
|
|
3912
|
+
${pc17.red("Out of sync:")} ${stale} file(s) would change. Run ${pc17.bold("flare sync-types")}.`);
|
|
3739
3913
|
return stale + problems > 0 ? 1 : 0;
|
|
3740
3914
|
}
|
|
3741
|
-
if (changed.length === 0 && problems === 0) log(
|
|
3915
|
+
if (changed.length === 0 && problems === 0) log(pc17.green("Everything is in sync."));
|
|
3742
3916
|
return problems > 0 ? 1 : 0;
|
|
3743
3917
|
}
|
|
3744
3918
|
|
|
3745
3919
|
// src/commands/user-role.ts
|
|
3746
|
-
import
|
|
3920
|
+
import pc18 from "picocolors";
|
|
3747
3921
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
3748
3922
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
3749
3923
|
function userRoleSql(email, role) {
|
|
@@ -3757,8 +3931,8 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
3757
3931
|
const sql2 = userRoleSql(email, role);
|
|
3758
3932
|
const roles = await listRoles(options2);
|
|
3759
3933
|
if (roles && !roles.includes(role)) {
|
|
3760
|
-
log(
|
|
3761
|
-
log(
|
|
3934
|
+
log(pc18.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
3935
|
+
log(pc18.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
|
|
3762
3936
|
return 1;
|
|
3763
3937
|
}
|
|
3764
3938
|
const db = readD1Databases(appRoot)[0];
|
|
@@ -3772,19 +3946,21 @@ ${result.output.trim()}`);
|
|
|
3772
3946
|
const [first] = extractJson(result.output);
|
|
3773
3947
|
const updated = first?.results ?? [];
|
|
3774
3948
|
if (updated.length === 0) {
|
|
3775
|
-
log(
|
|
3949
|
+
log(pc18.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
|
|
3776
3950
|
return 1;
|
|
3777
3951
|
}
|
|
3778
|
-
log(
|
|
3952
|
+
log(pc18.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
3779
3953
|
return 0;
|
|
3780
3954
|
}
|
|
3781
3955
|
|
|
3782
3956
|
// src/cli.ts
|
|
3783
3957
|
function createCli() {
|
|
3784
3958
|
const cli = cac("flare");
|
|
3785
|
-
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--auth
|
|
3959
|
+
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").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 --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(async (dir, options2) => {
|
|
3786
3960
|
const install = !options2.skipInstall;
|
|
3787
|
-
|
|
3961
|
+
let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
3962
|
+
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers);
|
|
3963
|
+
const result = createApp(dir, { pm: options2.pm, install, ...answers });
|
|
3788
3964
|
printNextSteps(result, install);
|
|
3789
3965
|
});
|
|
3790
3966
|
cli.command("gen <generator> [name]", "Generate code. Generators: resource, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
|
|
@@ -3829,6 +4005,7 @@ function createCli() {
|
|
|
3829
4005
|
cli.command("sync-types", "Regenerate schema, routes, clients, and validators from resource descriptors; report drift").option("--check", "Change nothing; exit 1 if anything is out of sync (CI)").option("--force", "Overwrite generated blocks that were edited by hand").action(async (options2) => {
|
|
3830
4006
|
process.exitCode = await syncTypes(options2);
|
|
3831
4007
|
});
|
|
4008
|
+
cli.command("theme [name]", "List the themes, or switch the app to one: default, coral, amber, sky, mono, emerald").example("flare theme").example("flare theme mono").action((name) => setTheme(name));
|
|
3832
4009
|
cli.command("tunnel [target]", "Share a local server on a public https://*.trycloudflare.com URL (Cloudflare Quick Tunnel; no account needed)").example("flare tunnel # http://localhost:3000").example("flare tunnel 8787").example("flare tunnel http://localhost:5173").action(async (target) => {
|
|
3833
4010
|
const local = toLocalUrl(target);
|
|
3834
4011
|
const tunnel = await openTunnel(local);
|
|
@@ -3864,6 +4041,6 @@ try {
|
|
|
3864
4041
|
}
|
|
3865
4042
|
}
|
|
3866
4043
|
} catch (error) {
|
|
3867
|
-
console.error(
|
|
4044
|
+
console.error(pc19.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
3868
4045
|
process.exitCode = 1;
|
|
3869
4046
|
}
|