@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.
Files changed (50) hide show
  1. package/dist/index.js +340 -163
  2. package/package.json +3 -2
  3. package/templates/app/app/api/openapi.json/route.ts +23 -0
  4. package/templates/app/app/api/reference/route.ts +36 -0
  5. package/templates/app/app/dashboard/account/page.tsx +38 -0
  6. package/templates/app/app/dashboard/layout.tsx +39 -0
  7. package/templates/app/app/dashboard/page.tsx +42 -9
  8. package/templates/app/app/forgot-password/page.tsx +22 -0
  9. package/templates/app/app/globals.css +109 -4
  10. package/templates/app/app/layout.tsx +28 -26
  11. package/templates/app/app/page.tsx +126 -13
  12. package/templates/app/app/reset-password/page.tsx +31 -0
  13. package/templates/app/app/sign-in/page.tsx +49 -33
  14. package/templates/app/app/sign-up/page.tsx +42 -23
  15. package/templates/app/app/two-factor/page.tsx +25 -0
  16. package/templates/app/components/account/security-settings.tsx +525 -0
  17. package/templates/app/components/admin/fields/country-field.tsx +152 -0
  18. package/templates/app/components/admin/fields/field-widget.tsx +186 -128
  19. package/templates/app/components/admin/fields/multi-select-field.tsx +46 -0
  20. package/templates/app/components/admin/fields/phone-field.tsx +86 -0
  21. package/templates/app/components/admin/resource-nav.tsx +120 -109
  22. package/templates/app/components/admin/resource-table.tsx +31 -1
  23. package/templates/app/components/auth/auth-shell.tsx +284 -0
  24. package/templates/app/components/auth/password-forms.tsx +79 -0
  25. package/templates/app/components/auth/provider-icons.tsx +39 -0
  26. package/templates/app/components/auth/sign-in-flow.tsx +315 -0
  27. package/templates/app/components/auth/sign-up-form.tsx +101 -0
  28. package/templates/app/components/auth/social-buttons.tsx +56 -0
  29. package/templates/app/components/auth/two-factor-form.tsx +124 -0
  30. package/templates/app/components/auth/ui.tsx +96 -0
  31. package/templates/app/components/dashboard/dashboard-nav.tsx +35 -0
  32. package/templates/app/components/marketing/product-preview.tsx +70 -0
  33. package/templates/app/components/marketing/site-header.tsx +39 -0
  34. package/templates/app/components/sign-out-button.tsx +1 -1
  35. package/templates/app/components/ui/checkbox.tsx +25 -0
  36. package/templates/app/components/ui/radio-group.tsx +29 -0
  37. package/templates/app/db/auth-schema.ts +167 -118
  38. package/templates/app/lib/admin-nav.ts +1 -1
  39. package/templates/app/lib/api-docs.ts +16 -0
  40. package/templates/app/lib/auth-client.ts +16 -1
  41. package/templates/app/lib/auth-config.ts +33 -0
  42. package/templates/app/lib/auth-emails.ts +94 -0
  43. package/templates/app/lib/auth.ts +81 -6
  44. package/templates/app/lib/site.ts +47 -0
  45. package/templates/app/lib/theme.ts +79 -0
  46. package/templates/app/migrations/0000_init.sql +32 -1
  47. package/templates/app/migrations/meta/0000_snapshot.json +215 -1
  48. package/templates/app/migrations/meta/_journal.json +1 -1
  49. package/templates/app/vite.config.ts +5 -3
  50. 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 pc18 from "picocolors";
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
- function parseAuthProviders(input) {
21
- if (!input) return [];
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 in AUTH_PROVIDERS));
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 socialProvidersCode(providers) {
32
- if (providers.length === 0) return "{}";
33
- const entries = providers.map((provider) => {
34
- const id = `env.${AUTH_PROVIDERS[provider].envPrefix}_CLIENT_ID`;
35
- const secret = `env.${AUTH_PROVIDERS[provider].envPrefix}_CLIENT_SECRET`;
36
- return ` ...(${id} && ${secret} ? { ${provider}: { clientId: ${id}, clientSecret: ${secret} } } : {}),`;
37
- });
38
- return [
39
- "{",
40
- " // Each provider switches on once its credentials are set: .dev.vars locally,",
41
- " // `wrangler secret put` in production.",
42
- ...entries,
43
- " }"
44
- ].join("\n");
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
- `# ${provider} OAuth app: ${console2}`,
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
- SOCIAL_PROVIDERS: socialProvidersCode(authProviders)
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 ALL_KINDS = [...SIMPLE_KINDS, "enum", "file", "belongsTo", "hasMany"];
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 (match = /^enum\((.*)\)$/.exec(type)) {
348
- field.kind = "enum";
349
- const options2 = list(match[1], "enum option", spec);
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}": enum option "${option}" may only use letters, digits, _ and -.`);
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 enum options.`);
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 existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2899
- import { dirname as dirname8, join as join13 } from "path";
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 pc8 from "picocolors";
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(pc8.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase4(name)}Id: belongsTo(${name}) field).`));
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 = join13(appRoot, relativePath);
2921
- const exists = existsSync14(path);
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
- writeFileSync8(path, renderDescriptor(name, fields));
2933
- log(`${pc8.green("create".padEnd(9))} ${relativePath}`);
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" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relativePath}`);
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(pc8.yellow(`
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(`${pc8.green("create".padEnd(9))} ${file}`);
2971
- for (const repair of result.repairs) log(pc8.dim(`repaired ${repair}`));
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: ${pc8.bold("flare migrate")} to apply it locally.`);
2974
- else if (exists) log(pc8.dim("\nNo table changes, so no migration."));
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 existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
2982
- import { basename as basename2, join as join14 } from "path";
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 pc9 from "picocolors";
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(`${pc9.green("create".padEnd(9))} ${file}`);
3001
- for (const repair of repairs) log(pc9.dim(`repaired ${repair}`));
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 = join14(appRoot, "migrations", "down");
3170
+ const downDir = join15(appRoot, "migrations", "down");
3004
3171
  mkdirSync8(downDir, { recursive: true });
3005
3172
  for (const file of files) {
3006
- const down = join14(downDir, basename2(file));
3007
- if (existsSync15(down)) continue;
3008
- writeFileSync9(down, DOWN_TEMPLATE(basename2(file)));
3009
- log(`${pc9.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
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 ${pc9.bold("flare migrate")}.`);
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: ${pc9.bold("flare migrate")}.`);
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 existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
3022
- import { dirname as dirname9, join as join15 } from "path";
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 pc10 from "picocolors";
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 = join15(appRoot, relative2);
3041
- const exists = existsSync16(path);
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
- writeFileSync10(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3054
- log(`${pc10.green("create".padEnd(9))} ${relative2}`);
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" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relative2}`);
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
- ${pc10.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
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 existsSync17, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync11 } from "fs";
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 join16 } from "path";
3078
- import pc11 from "picocolors";
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 = join16(appRoot, db.migrations_dir ?? "migrations");
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 = join16(migrationsDir, "down", name);
3205
- if (existsSync17(handWritten)) {
3206
- const sql2 = readFileSync10(handWritten, "utf8");
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 = join16(migrationsDir, name);
3214
- if (!existsSync17(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3215
- const down = deriveDown(readFileSync10(upPath, "utf8"));
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(`${pc11.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
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} ${pc11.dim(`(${step.source})`)}`);
3227
- for (const line of step.sql.trim().split("\n")) log(pc11.dim(` ${line}`));
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(pc11.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
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(join16(tmpdir(), "flare-rollback-"));
3402
+ const dir = mkdtempSync(join17(tmpdir(), "flare-rollback-"));
3236
3403
  try {
3237
- const file = join16(dir, "rollback.sql");
3238
- writeFileSync11(file, script);
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(pc11.green(`
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 existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync11, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3252
- import { dirname as dirname10, join as join17 } from "path";
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 pc12 from "picocolors";
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(join17(appRoot, start));
3272
- const roots = [join17(appRoot, "app", "api"), join17(appRoot, "app", "admin")];
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() && existsSync18(dir) && readdirSync7(dir).length === 0) {
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 (existsSync18(join17(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
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 = readFileSync11(join17(appRoot, path), "utf8");
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(join17(appRoot, path));
3500
+ unlinkSync2(join18(appRoot, path));
3334
3501
  removeEmptyDirs(appRoot, path);
3335
- log(`${pc12.red("remove".padEnd(9))} ${path}`);
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 && existsSync18(join17(appRoot, "seeds"))) {
3340
- const seeds = readdirSync7(join17(appRoot, "seeds")).filter(
3341
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(join17(appRoot, "seeds", file), "utf8"))
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(pc12.yellow(`
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(`${pc12.green("create".padEnd(9))} ${file}`);
3351
- for (const repair of result.repairs) log(pc12.dim(`repaired ${repair}`));
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: ${pc12.bold("flare migrate")} to drop the table locally.`);
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 pc13 from "picocolors";
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(pc13.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3406
- log(pc13.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
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 existsSync19, mkdirSync as mkdirSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync12 } from "fs";
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 join18 } from "path";
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 pc14 from "picocolors";
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 = join18(appRoot, SEEDS_DIR);
3422
- const all = existsSync19(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
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 ${pc14.bold("flare seed:make <name>")}.`);
3604
+ log(`No seeds found. Create one with ${pc15.bold("flare seed:make <name>")}.`);
3438
3605
  return;
3439
3606
  }
3440
- const jiti = createJiti4(join18(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3441
- const appRequire = createRequire(join18(appRoot, "package.json"));
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(join18(appRoot, "db/schema.ts"));
3611
+ const schema = await jiti.import(join19(appRoot, "db/schema.ts"));
3445
3612
  const { env, dispose } = await wrangler.getPlatformProxy({
3446
- configPath: join18(appRoot, "wrangler.jsonc"),
3447
- persist: { path: join18(appRoot, LOCAL_STATE) }
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(join18(appRoot, SEEDS_DIR, file));
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(`${pc14.cyan("seed")} ${file}`);
3623
+ log(`${pc15.cyan("seed")} ${file}`);
3457
3624
  await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
3458
3625
  }
3459
- log(pc14.green(`
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 = join18(appRoot, relative2);
3529
- if (existsSync19(path)) throw new Error(`${relative2} already exists.`);
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(join18(appRoot, SEEDS_DIR), { recursive: true });
3539
- writeFileSync12(path, renderSeed(resource));
3540
- log(`${pc14.green("create".padEnd(9))} ${relative2}`);
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 ${pc14.bold(`flare seed ${name}`)}.`);
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 existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "fs";
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 join19 } from "path";
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 pc15 from "picocolors";
3727
+ import pc16 from "picocolors";
3554
3728
  function stripeKeys(appRoot) {
3555
3729
  const map = {};
3556
- const path = join19(appRoot, ".dev.vars");
3557
- if (existsSync20(path)) {
3558
- for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
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(join19(appRoot, "package.json"));
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(readFileSync12(join19(appRoot, "package.json"), "utf8")).name ?? "app";
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(join19(tmpdir2(), "flare-sync-"));
3839
+ const dir = mkdtempSync2(join20(tmpdir2(), "flare-sync-"));
3666
3840
  try {
3667
- const file = join19(dir, "sync-plans.sql");
3668
- writeFileSync13(file, renderSyncSql(rows));
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 ? pc15.green("create") : pc15.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc15.dim(price)}`);
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(pc15.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
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(`${pc15.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
3685
- log(pc15.green(`
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 existsSync21 } from "fs";
3692
- import { join as join20 } from "path";
3693
- import pc16 from "picocolors";
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 || existsSync21(join20(appRoot, "policies/index.ts"));
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
- ${pc16.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
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
- ${pc16.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
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
- ${pc16.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
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 ${pc16.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
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
- ${pc16.cyan("Tables changed.")} Create a migration with ${pc16.bold("flare gen migration <name> --from-schema")}, then ${pc16.bold("flare migrate")}.`);
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(pc16.green("Generated files are in sync with the resource descriptors."));
3910
+ if (stale === 0 && problems === 0) log(pc17.green("Generated files are in sync with the resource descriptors."));
3737
3911
  else log(`
3738
- ${pc16.red("Out of sync:")} ${stale} file(s) would change. Run ${pc16.bold("flare sync-types")}.`);
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(pc16.green("Everything is in sync."));
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 pc17 from "picocolors";
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(pc17.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3761
- log(pc17.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
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(pc17.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
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(pc17.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
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-providers <list>", "OAuth providers to scaffold, comma-separated (google, github)").option("--skip-install", "Write files without installing dependencies").example("flare create shop --auth-providers google,github").action((dir, options2) => {
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
- const result = createApp(dir, { pm: options2.pm, install, authProviders: options2.authProviders });
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(pc18.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
4044
+ console.error(pc19.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
3868
4045
  process.exitCode = 1;
3869
4046
  }