@flaredev/cli 0.1.1 → 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 (51) hide show
  1. package/README.md +47 -47
  2. package/dist/index.js +373 -169
  3. package/package.json +3 -2
  4. package/templates/app/app/api/openapi.json/route.ts +23 -0
  5. package/templates/app/app/api/reference/route.ts +36 -0
  6. package/templates/app/app/dashboard/account/page.tsx +38 -0
  7. package/templates/app/app/dashboard/layout.tsx +39 -0
  8. package/templates/app/app/dashboard/page.tsx +42 -9
  9. package/templates/app/app/forgot-password/page.tsx +22 -0
  10. package/templates/app/app/globals.css +109 -4
  11. package/templates/app/app/layout.tsx +28 -26
  12. package/templates/app/app/page.tsx +126 -13
  13. package/templates/app/app/reset-password/page.tsx +31 -0
  14. package/templates/app/app/sign-in/page.tsx +49 -33
  15. package/templates/app/app/sign-up/page.tsx +42 -23
  16. package/templates/app/app/two-factor/page.tsx +25 -0
  17. package/templates/app/components/account/security-settings.tsx +525 -0
  18. package/templates/app/components/admin/fields/country-field.tsx +152 -0
  19. package/templates/app/components/admin/fields/field-widget.tsx +186 -128
  20. package/templates/app/components/admin/fields/multi-select-field.tsx +46 -0
  21. package/templates/app/components/admin/fields/phone-field.tsx +86 -0
  22. package/templates/app/components/admin/resource-nav.tsx +120 -109
  23. package/templates/app/components/admin/resource-table.tsx +31 -1
  24. package/templates/app/components/auth/auth-shell.tsx +284 -0
  25. package/templates/app/components/auth/password-forms.tsx +79 -0
  26. package/templates/app/components/auth/provider-icons.tsx +39 -0
  27. package/templates/app/components/auth/sign-in-flow.tsx +315 -0
  28. package/templates/app/components/auth/sign-up-form.tsx +101 -0
  29. package/templates/app/components/auth/social-buttons.tsx +56 -0
  30. package/templates/app/components/auth/two-factor-form.tsx +124 -0
  31. package/templates/app/components/auth/ui.tsx +96 -0
  32. package/templates/app/components/dashboard/dashboard-nav.tsx +35 -0
  33. package/templates/app/components/marketing/product-preview.tsx +70 -0
  34. package/templates/app/components/marketing/site-header.tsx +39 -0
  35. package/templates/app/components/sign-out-button.tsx +1 -1
  36. package/templates/app/components/ui/checkbox.tsx +25 -0
  37. package/templates/app/components/ui/radio-group.tsx +29 -0
  38. package/templates/app/db/auth-schema.ts +167 -118
  39. package/templates/app/lib/admin-nav.ts +1 -1
  40. package/templates/app/lib/api-docs.ts +16 -0
  41. package/templates/app/lib/auth-client.ts +16 -1
  42. package/templates/app/lib/auth-config.ts +33 -0
  43. package/templates/app/lib/auth-emails.ts +94 -0
  44. package/templates/app/lib/auth.ts +81 -6
  45. package/templates/app/lib/site.ts +47 -0
  46. package/templates/app/lib/theme.ts +79 -0
  47. package/templates/app/migrations/0000_init.sql +32 -1
  48. package/templates/app/migrations/meta/0000_snapshot.json +215 -1
  49. package/templates/app/migrations/meta/_journal.json +1 -1
  50. package/templates/app/vite.config.ts +5 -3
  51. 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",
@@ -157,6 +234,15 @@ function childEnv(env = process.env) {
157
234
  }
158
235
  return next;
159
236
  }
237
+ function runQuiet(command, args, cwd) {
238
+ const env = childEnv();
239
+ const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, env, shell: true, encoding: "utf8" }) : spawnSync(command, args, { cwd, env, encoding: "utf8" });
240
+ return { code: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
241
+ }
242
+ function installArgs(pm) {
243
+ if (pm === "npm") return ["install", "--no-audit", "--no-fund", "--loglevel=error"];
244
+ return ["install"];
245
+ }
160
246
 
161
247
  // src/commands/create.ts
162
248
  function toAppName(dir) {
@@ -185,14 +271,17 @@ function createApp(target, options2 = {}) {
185
271
  }
186
272
  const packageManager = options2.pm ?? detectPackageManager();
187
273
  const authProviders = parseAuthProviders(options2.authProviders);
274
+ const authMethods = parseAuthMethods(options2.auth);
275
+ const theme = parseTheme(options2.theme);
188
276
  const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
189
277
  const compatibilityDate = options2.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
190
278
  copyTemplate(join2(templatesDir, "app"), dir, {
191
279
  APP_NAME: name,
192
280
  COMPAT_DATE: compatibilityDate,
193
281
  PM: packageManager,
194
- SOCIAL_PROVIDERS: socialProvidersCode(authProviders)
282
+ THEME: theme
195
283
  });
284
+ writeFileSync2(join2(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
196
285
  appendFileSync(join2(dir, ".dev.vars.example"), devVarsExampleEntries(authProviders));
197
286
  writeJson(join2(dir, "package.json"), {
198
287
  name,
@@ -221,24 +310,42 @@ ${devVarsEntries(authProviders)}`
221
310
  if (packageManager === "pnpm" && !inWorkspace) {
222
311
  writeFileSync2(join2(dir, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n");
223
312
  }
313
+ const log = options2.log ?? ((message) => console.log(message));
224
314
  if (options2.install !== false) {
225
- const code = run(packageManager, ["install"], dir);
315
+ log(`${pc.green("\u2714")} Wrote the app to ${relative(process.cwd(), dir) || "."}`);
316
+ log(`${pc.cyan("\u25CF")} Installing dependencies with ${packageManager}. The first install downloads the Workers runtime and toolchain, so it can take a few minutes.
317
+ `);
318
+ const started = Date.now();
319
+ const code = run(packageManager, installArgs(packageManager), dir);
226
320
  if (code !== 0) throw new Error(`${packageManager} install failed (exit code ${code}).`);
227
- const typegen = run(packageManager, ["run", "cf-typegen"], dir);
228
- if (typegen !== 0) throw new Error(`wrangler types failed (exit code ${typegen}).`);
321
+ log(`${pc.green("\u2714")} Installed dependencies ${pc.dim(`(${formatDuration(Date.now() - started)})`)}`);
322
+ const typegen = runQuiet(packageManager, ["run", "cf-typegen"], dir);
323
+ if (typegen.code !== 0) {
324
+ process.stderr.write(typegen.output);
325
+ throw new Error(`wrangler types failed (exit code ${typegen.code}).`);
326
+ }
327
+ log(`${pc.green("\u2714")} Generated types for the Cloudflare bindings`);
229
328
  }
230
329
  return { dir, name, packageManager, inWorkspace };
231
330
  }
331
+ function formatDuration(ms) {
332
+ const seconds = Math.round(ms / 1e3);
333
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
334
+ }
232
335
  function printNextSteps(result, installed) {
233
336
  const rel = relative(process.cwd(), result.dir) || ".";
234
337
  const pm = result.packageManager;
338
+ const step = (command, note) => ` ${command.padEnd(30)}${pc.dim(note)}`;
235
339
  console.log(`
236
- ${pc.green("\u2714")} Created ${pc.bold(result.name)} in ${rel}
340
+ ${pc.green("\u2714")} Created ${pc.bold(result.name)}
237
341
  `);
238
342
  console.log("Next steps:");
239
343
  console.log(` cd ${rel}`);
240
344
  if (!installed) console.log(` ${pm} install`);
241
- console.log(` ${pm} run dev`);
345
+ console.log(step(`${pm} run dev`, "start it at http://localhost:3000"));
346
+ console.log(step("npx flare gen resource ...", "add your first resource"));
347
+ console.log(`
348
+ Quickstart: ${pc.cyan("https://flare-docs.codetotech.com/start/quickstart/")}`);
242
349
  }
243
350
 
244
351
  // src/commands/gen-billing.ts
@@ -247,9 +354,10 @@ import { dirname as dirname6, join as join11 } from "path";
247
354
  import pc6 from "picocolors";
248
355
 
249
356
  // src/generator/grammar.ts
250
- import { camelCase, FILE_CATEGORIES } from "@flaredev/core";
357
+ import { camelCase, FILE_CATEGORIES, STRING_FORMATS } from "@flaredev/core";
251
358
  var SIMPLE_KINDS = ["string", "text", "int", "float", "boolean", "date", "datetime"];
252
- 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"];
253
361
  var FieldGrammarError = class extends Error {
254
362
  constructor(message) {
255
363
  super(message);
@@ -317,15 +425,21 @@ function parseField(spec) {
317
425
  let match;
318
426
  if (SIMPLE_KINDS.includes(type)) {
319
427
  field.kind = type;
320
- } else if (match = /^enum\((.*)\)$/.exec(type)) {
321
- field.kind = "enum";
322
- 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);
323
437
  for (const option of options2) {
324
438
  if (!/^[A-Za-z0-9_-]+$/.test(option)) {
325
- 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 -.`);
326
440
  }
327
441
  }
328
- 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.`);
329
443
  field.options = options2;
330
444
  } else if (match = /^file(?::\[(.*)\])?$/.exec(type)) {
331
445
  field.kind = "file";
@@ -360,7 +474,7 @@ function parseField(spec) {
360
474
  const base = type.replace(/[([].*$/, "").replace(/:$/, "");
361
475
  throw new FieldGrammarError(`"${spec}": unknown type "${type}".${suggest(base, ALL_KINDS)} Types: ${ALL_KINDS.join(", ")}.`);
362
476
  }
363
- if (field.kind === "string") {
477
+ if (field.kind === "string" && !field.format) {
364
478
  if (/^email$|Email$/.test(key)) field.format = "email";
365
479
  else if (/^(url|website)$|(Url|Website)$/.test(key)) field.format = "url";
366
480
  }
@@ -478,6 +592,7 @@ function options(field) {
478
592
  if (!field.required) opts.required = false;
479
593
  if (field.unique) opts.unique = true;
480
594
  if (field.format) opts.format = field.format;
595
+ if (field.widget) opts.widget = field.widget;
481
596
  if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
482
597
  return opts;
483
598
  }
@@ -486,6 +601,8 @@ function toField(field) {
486
601
  switch (field.kind) {
487
602
  case "enum":
488
603
  return builders.enum(field.options, opts);
604
+ case "multiselect":
605
+ return builders.multiselect(field.options, opts);
489
606
  case "file":
490
607
  return builders.file(field.accept, opts);
491
608
  case "belongsTo":
@@ -507,6 +624,8 @@ function renderField(field) {
507
624
  switch (field.kind) {
508
625
  case "enum":
509
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)}),`;
510
629
  case "file":
511
630
  return `${field.key}: field.file(${args(literal(field.accept), opts)}),`;
512
631
  case "belongsTo":
@@ -1816,6 +1935,9 @@ function columnExpression(key, def, resources, selfName) {
1816
1935
  case "enum":
1817
1936
  expr = `text(${col}, { enum: ${q(def.options)} })`;
1818
1937
  break;
1938
+ case "multiselect":
1939
+ expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q(option)).join(" | ")})[]>()`;
1940
+ break;
1819
1941
  case "belongsTo": {
1820
1942
  const target = resources.get(def.target);
1821
1943
  if (!target) {
@@ -1829,7 +1951,7 @@ function columnExpression(key, def, resources, selfName) {
1829
1951
  expr = `text(${col})`;
1830
1952
  }
1831
1953
  if (def.required) expr += ".notNull()";
1832
- if (def.unique) expr += ".unique()";
1954
+ if ("unique" in def && def.unique) expr += ".unique()";
1833
1955
  if ("default" in def && def.default !== void 0) expr += `.default(${q(def.default)})`;
1834
1956
  return expr;
1835
1957
  }
@@ -2867,11 +2989,83 @@ async function genSecurity(options2 = {}) {
2867
2989
  return { files, migrations, bindings };
2868
2990
  }
2869
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
+
2870
3064
  // src/commands/gen.ts
2871
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2872
- 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";
2873
3067
  import { camelCase as camelCase4, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
2874
- import pc8 from "picocolors";
3068
+ import pc9 from "picocolors";
2875
3069
  function validateFields(name, fields, others, log) {
2876
3070
  const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
2877
3071
  for (const field of fields) {
@@ -2882,7 +3076,7 @@ function validateFields(name, fields, others, log) {
2882
3076
  const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
2883
3077
  const graph = relationGraph2([...others, candidate]);
2884
3078
  for (const pending of graph.pending.filter((p) => p.resource === name)) {
2885
- 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).`));
2886
3080
  }
2887
3081
  }
2888
3082
  async function genResource(rawName, options2) {
@@ -2890,8 +3084,8 @@ async function genResource(rawName, options2) {
2890
3084
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
2891
3085
  const name = resourceName(rawName);
2892
3086
  const relativePath = descriptorPath(name);
2893
- const path = join13(appRoot, relativePath);
2894
- const exists = existsSync14(path);
3087
+ const path = join14(appRoot, relativePath);
3088
+ const exists = existsSync15(path);
2895
3089
  if (!exists && !options2.fields) {
2896
3090
  throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
2897
3091
  }
@@ -2902,12 +3096,12 @@ async function genResource(rawName, options2) {
2902
3096
  validateFields(name, fields, others, log);
2903
3097
  if (!exists) {
2904
3098
  mkdirSync7(dirname8(path), { recursive: true });
2905
- writeFileSync8(path, renderDescriptor(name, fields));
2906
- log(`${pc8.green("create".padEnd(9))} ${relativePath}`);
3099
+ writeFileSync9(path, renderDescriptor(name, fields));
3100
+ log(`${pc9.green("create".padEnd(9))} ${relativePath}`);
2907
3101
  } else {
2908
3102
  try {
2909
3103
  const outcome = writeGenerated(path, renderFieldsBlock(fields), { force: options2.force });
2910
- log(`${(outcome === "identical" ? pc8.dim : pc8.yellow)(outcome.padEnd(9))} ${relativePath}`);
3104
+ log(`${(outcome === "identical" ? pc9.dim : pc9.yellow)(outcome.padEnd(9))} ${relativePath}`);
2911
3105
  } catch (error) {
2912
3106
  if (error instanceof DriftError) {
2913
3107
  throw new Error(
@@ -2933,28 +3127,28 @@ async function genResource(rawName, options2) {
2933
3127
  }
2934
3128
  const skipped = written.filter((r) => r.outcome.startsWith("skipped"));
2935
3129
  if (skipped.length) {
2936
- log(pc8.yellow(`
3130
+ log(pc9.yellow(`
2937
3131
  Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for details.`));
2938
3132
  }
2939
3133
  let migrations = [];
2940
3134
  if (!options2.skipMigration) {
2941
3135
  const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
2942
3136
  migrations = result.files;
2943
- for (const file of migrations) log(`${pc8.green("create".padEnd(9))} ${file}`);
2944
- for (const repair of result.repairs) log(pc8.dim(`repaired ${repair}`));
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}`));
2945
3139
  if (migrations.length) log(`
2946
- Next: ${pc8.bold("flare migrate")} to apply it locally.`);
2947
- 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."));
2948
3142
  }
2949
3143
  const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
2950
3144
  return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
2951
3145
  }
2952
3146
 
2953
3147
  // src/commands/gen-migration.ts
2954
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
2955
- 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";
2956
3150
  import { snakeCase } from "@flaredev/core";
2957
- import pc9 from "picocolors";
3151
+ import pc10 from "picocolors";
2958
3152
  var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
2959
3153
  -- Write SQL that undoes the up migration, e.g.:
2960
3154
  -- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
@@ -2970,31 +3164,31 @@ async function genMigration(rawName, options2 = {}) {
2970
3164
  log(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
2971
3165
  return [];
2972
3166
  }
2973
- for (const file of files) log(`${pc9.green("create".padEnd(9))} ${file}`);
2974
- 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}`));
2975
3169
  if (!options2.fromSchema) {
2976
- const downDir = join14(appRoot, "migrations", "down");
3170
+ const downDir = join15(appRoot, "migrations", "down");
2977
3171
  mkdirSync8(downDir, { recursive: true });
2978
3172
  for (const file of files) {
2979
- const down = join14(downDir, basename2(file));
2980
- if (existsSync15(down)) continue;
2981
- writeFileSync9(down, DOWN_TEMPLATE(basename2(file)));
2982
- log(`${pc9.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
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)}`);
2983
3177
  }
2984
3178
  log(`
2985
- 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")}.`);
2986
3180
  } else {
2987
3181
  log(`
2988
- Next: ${pc9.bold("flare migrate")}.`);
3182
+ Next: ${pc10.bold("flare migrate")}.`);
2989
3183
  }
2990
3184
  return files;
2991
3185
  }
2992
3186
 
2993
3187
  // src/commands/gen-policy.ts
2994
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
2995
- 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";
2996
3190
  import { definePolicy } from "@flaredev/core";
2997
- import pc10 from "picocolors";
3191
+ import pc11 from "picocolors";
2998
3192
  function parseRoles(input, what = "--roles") {
2999
3193
  const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
3000
3194
  if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
@@ -3010,8 +3204,8 @@ async function genPolicy(rawName, options2) {
3010
3204
  throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
3011
3205
  }
3012
3206
  const relative2 = policyPath(name);
3013
- const path = join15(appRoot, relative2);
3014
- const exists = existsSync16(path);
3207
+ const path = join16(appRoot, relative2);
3208
+ const exists = existsSync17(path);
3015
3209
  let roles;
3016
3210
  let deleteRoles;
3017
3211
  if (options2.roles || !exists) {
@@ -3023,12 +3217,12 @@ async function genPolicy(rawName, options2) {
3023
3217
  const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
3024
3218
  if (!exists) {
3025
3219
  mkdirSync9(dirname9(path), { recursive: true });
3026
- writeFileSync10(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3027
- log(`${pc10.green("create".padEnd(9))} ${relative2}`);
3220
+ writeFileSync11(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
3221
+ log(`${pc11.green("create".padEnd(9))} ${relative2}`);
3028
3222
  } else {
3029
3223
  try {
3030
3224
  const outcome = writeGenerated(path, block, { force: options2.force });
3031
- log(`${(outcome === "identical" ? pc10.dim : pc10.yellow)(outcome.padEnd(9))} ${relative2}`);
3225
+ log(`${(outcome === "identical" ? pc11.dim : pc11.yellow)(outcome.padEnd(9))} ${relative2}`);
3032
3226
  } catch (error) {
3033
3227
  if (error instanceof DriftError) {
3034
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.`);
@@ -3040,15 +3234,15 @@ async function genPolicy(rawName, options2) {
3040
3234
  const policies = await loadPolicies(appRoot);
3041
3235
  logResults(applyPlan(appRoot, planFiles(resources, policies)), log);
3042
3236
  log(`
3043
- ${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.")}`);
3044
3238
  return { name, path: relative2 };
3045
3239
  }
3046
3240
 
3047
3241
  // src/commands/migrate.ts
3048
- 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";
3049
3243
  import { tmpdir } from "os";
3050
- import { join as join16 } from "path";
3051
- import pc11 from "picocolors";
3244
+ import { join as join17 } from "path";
3245
+ import pc12 from "picocolors";
3052
3246
 
3053
3247
  // src/generator/down.ts
3054
3248
  var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
@@ -3165,7 +3359,7 @@ async function rollback(options2 = {}) {
3165
3359
  const steps = options2.steps ?? 1;
3166
3360
  if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
3167
3361
  const { appRoot, db, target, where, wrangler, log } = context(options2);
3168
- const migrationsDir = join16(appRoot, db.migrations_dir ?? "migrations");
3362
+ const migrationsDir = join17(appRoot, db.migrations_dir ?? "migrations");
3169
3363
  const applied = await appliedMigrations(options2, steps);
3170
3364
  if (applied.length === 0) {
3171
3365
  log("Nothing to roll back.");
@@ -3174,18 +3368,18 @@ async function rollback(options2 = {}) {
3174
3368
  const plan = [];
3175
3369
  for (const { name } of applied) {
3176
3370
  if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
3177
- const handWritten = join16(migrationsDir, "down", name);
3178
- if (existsSync17(handWritten)) {
3179
- const sql2 = readFileSync10(handWritten, "utf8");
3371
+ const handWritten = join17(migrationsDir, "down", name);
3372
+ if (existsSync18(handWritten)) {
3373
+ const sql2 = readFileSync11(handWritten, "utf8");
3180
3374
  if (splitStatements(sql2).length === 0) {
3181
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.`);
3182
3376
  }
3183
3377
  plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
3184
3378
  continue;
3185
3379
  }
3186
- const upPath = join16(migrationsDir, name);
3187
- if (!existsSync17(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
3188
- 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"));
3189
3383
  if (!down.reversible) {
3190
3384
  throw new Error(
3191
3385
  `Can't roll back ${name}: ${down.reason}
@@ -3194,37 +3388,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
3194
3388
  }
3195
3389
  plan.push({ name, sql: down.sql, source: "derived" });
3196
3390
  }
3197
- 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}):`)}`);
3198
3392
  for (const step of plan) {
3199
- log(` ${step.name} ${pc11.dim(`(${step.source})`)}`);
3200
- 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}`));
3201
3395
  }
3202
3396
  if (options2.remote && !options2.yes) {
3203
- 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."));
3204
3398
  return 1;
3205
3399
  }
3206
3400
  const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
3207
3401
  DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
3208
- const dir = mkdtempSync(join16(tmpdir(), "flare-rollback-"));
3402
+ const dir = mkdtempSync(join17(tmpdir(), "flare-rollback-"));
3209
3403
  try {
3210
- const file = join16(dir, "rollback.sql");
3211
- writeFileSync11(file, script);
3404
+ const file = join17(dir, "rollback.sql");
3405
+ writeFileSync12(file, script);
3212
3406
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3213
3407
  if (result.code !== 0) throw new Error(`Rollback failed:
3214
3408
  ${result.output.trim()}`);
3215
3409
  } finally {
3216
3410
  rmSync2(dir, { recursive: true, force: true });
3217
3411
  }
3218
- log(pc11.green(`
3412
+ log(pc12.green(`
3219
3413
  Rolled back ${plan.map((step) => step.name).join(", ")}.`));
3220
3414
  return 0;
3221
3415
  }
3222
3416
 
3223
3417
  // src/commands/rm.ts
3224
- import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync11, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
3225
- import { dirname as dirname10, join as join17 } from "path";
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";
3226
3420
  import { relationGraph as relationGraph3 } from "@flaredev/core";
3227
- import pc12 from "picocolors";
3421
+ import pc13 from "picocolors";
3228
3422
  var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
3229
3423
  function handWrittenReason(source, header, options2 = {}) {
3230
3424
  const parts = splitMarkers(source);
@@ -3241,10 +3435,10 @@ function handWrittenReason(source, header, options2 = {}) {
3241
3435
  return null;
3242
3436
  }
3243
3437
  function removeEmptyDirs(appRoot, start) {
3244
- let dir = dirname10(join17(appRoot, start));
3245
- 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")];
3246
3440
  const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
3247
- while (inside() && existsSync18(dir) && readdirSync7(dir).length === 0) {
3441
+ while (inside() && existsSync19(dir) && readdirSync7(dir).length === 0) {
3248
3442
  rmdirSync(dir);
3249
3443
  dir = dirname10(dir);
3250
3444
  }
@@ -3273,7 +3467,7 @@ async function rmResource(rawName, options2 = {}) {
3273
3467
  if (entry) {
3274
3468
  owned.add(entry.file);
3275
3469
  for (const file of planFiles(all)) {
3276
- 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);
3277
3471
  }
3278
3472
  }
3279
3473
  for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
@@ -3282,7 +3476,7 @@ async function rmResource(rawName, options2 = {}) {
3282
3476
  if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
3283
3477
  const findings = [];
3284
3478
  for (const path of owned) {
3285
- const source = readFileSync11(join17(appRoot, path), "utf8");
3479
+ const source = readFileSync12(join18(appRoot, path), "utf8");
3286
3480
  let wrapper;
3287
3481
  if (entry && path === entry.file) {
3288
3482
  const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
@@ -3303,34 +3497,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
3303
3497
  }
3304
3498
  const removed = [...owned].sort();
3305
3499
  for (const path of removed) {
3306
- unlinkSync2(join17(appRoot, path));
3500
+ unlinkSync2(join18(appRoot, path));
3307
3501
  removeEmptyDirs(appRoot, path);
3308
- log(`${pc12.red("remove".padEnd(9))} ${path}`);
3502
+ log(`${pc13.red("remove".padEnd(9))} ${path}`);
3309
3503
  }
3310
3504
  logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log);
3311
3505
  const table = entry ? tableExport(entry.resource) : void 0;
3312
- if (table && existsSync18(join17(appRoot, "seeds"))) {
3313
- const seeds = readdirSync7(join17(appRoot, "seeds")).filter(
3314
- (file) => new RegExp(`\\b${table}\\b`).test(readFileSync11(join17(appRoot, "seeds", file), "utf8"))
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"))
3315
3509
  );
3316
- if (seeds.length) log(pc12.yellow(`
3510
+ if (seeds.length) log(pc13.yellow(`
3317
3511
  These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
3318
3512
  }
3319
3513
  let migrations = [];
3320
3514
  if (!options2.skipMigration && entry) {
3321
3515
  const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
3322
3516
  migrations = result.files;
3323
- for (const file of migrations) log(`${pc12.green("create".padEnd(9))} ${file}`);
3324
- for (const repair of result.repairs) log(pc12.dim(`repaired ${repair}`));
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}`));
3325
3519
  if (migrations.length) log(`
3326
- Next: ${pc12.bold("flare migrate")} to drop the table locally.`);
3520
+ Next: ${pc13.bold("flare migrate")} to drop the table locally.`);
3327
3521
  }
3328
3522
  return { removed, migrations };
3329
3523
  }
3330
3524
 
3331
3525
  // src/commands/role.ts
3332
3526
  import { humanize } from "@flaredev/core";
3333
- import pc13 from "picocolors";
3527
+ import pc14 from "picocolors";
3334
3528
  var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
3335
3529
  function roleInsertSql(name, label) {
3336
3530
  if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
@@ -3375,24 +3569,24 @@ ${result.output.trim()}`);
3375
3569
  }
3376
3570
  const [first] = extractJson(result.output);
3377
3571
  const added = first?.results?.[0];
3378
- log(pc13.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
3379
- log(pc13.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
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" : ""}`));
3380
3574
  return 0;
3381
3575
  }
3382
3576
 
3383
3577
  // src/commands/seed.ts
3384
- 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";
3385
3579
  import { createRequire } from "module";
3386
- import { join as join18 } from "path";
3580
+ import { join as join19 } from "path";
3387
3581
  import { pathToFileURL } from "url";
3388
3582
  import { kebabCase as kebabCase3, storedFields as storedFields2 } from "@flaredev/core";
3389
3583
  import { createJiti as createJiti4 } from "jiti";
3390
- import pc14 from "picocolors";
3584
+ import pc15 from "picocolors";
3391
3585
  var SEEDS_DIR = "seeds";
3392
3586
  var LOCAL_STATE = ".wrangler/state/v3";
3393
3587
  function seedFiles(appRoot, names) {
3394
- const dir = join18(appRoot, SEEDS_DIR);
3395
- 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() : [];
3396
3590
  if (!names?.length) return all;
3397
3591
  return names.map((name) => {
3398
3592
  const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
@@ -3407,29 +3601,29 @@ async function runSeeds(options2 = {}) {
3407
3601
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3408
3602
  const files = seedFiles(appRoot, options2.names);
3409
3603
  if (files.length === 0) {
3410
- 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>")}.`);
3411
3605
  return;
3412
3606
  }
3413
- const jiti = createJiti4(join18(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
3414
- 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"));
3415
3609
  const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
3416
3610
  const { drizzle } = await jiti.import("drizzle-orm/d1");
3417
- const schema = await jiti.import(join18(appRoot, "db/schema.ts"));
3611
+ const schema = await jiti.import(join19(appRoot, "db/schema.ts"));
3418
3612
  const { env, dispose } = await wrangler.getPlatformProxy({
3419
- configPath: join18(appRoot, "wrangler.jsonc"),
3420
- persist: { path: join18(appRoot, LOCAL_STATE) }
3613
+ configPath: join19(appRoot, "wrangler.jsonc"),
3614
+ persist: { path: join19(appRoot, LOCAL_STATE) }
3421
3615
  });
3422
3616
  try {
3423
3617
  const bindings = env;
3424
3618
  if (!bindings.DB) throw new Error("No DB binding found in wrangler.jsonc.");
3425
3619
  const db = drizzle(bindings.DB, { schema });
3426
3620
  for (const file of files) {
3427
- const mod = await jiti.import(join18(appRoot, SEEDS_DIR, file));
3621
+ const mod = await jiti.import(join19(appRoot, SEEDS_DIR, file));
3428
3622
  if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
3429
- log(`${pc14.cyan("seed")} ${file}`);
3623
+ log(`${pc15.cyan("seed")} ${file}`);
3430
3624
  await mod.default({ db, env: bindings, log: (message) => log(` ${message}`) });
3431
3625
  }
3432
- log(pc14.green(`
3626
+ log(pc15.green(`
3433
3627
  Ran ${files.length} seed(s) against the local database.`));
3434
3628
  } finally {
3435
3629
  await dispose();
@@ -3440,6 +3634,11 @@ function sampleValue(key, def, row) {
3440
3634
  case "string":
3441
3635
  if (def.format === "email") return `\`${kebabCase3(key).replace(/-?email$/, "") || "user"}\${i}@example.com\``;
3442
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}\``;
3443
3642
  return `\`${def.label} \${i}\``;
3444
3643
  case "text":
3445
3644
  return `"Lorem ipsum dolor sit amet."`;
@@ -3455,6 +3654,8 @@ function sampleValue(key, def, row) {
3455
3654
  return "new Date().toISOString()";
3456
3655
  case "enum":
3457
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)));
3458
3659
  case "file":
3459
3660
  return void 0;
3460
3661
  case "belongsTo":
@@ -3498,8 +3699,8 @@ async function makeSeed(rawName, options2 = {}) {
3498
3699
  const name = kebabCase3(rawName);
3499
3700
  if (!name) throw new Error(`Invalid seed name "${rawName}".`);
3500
3701
  const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
3501
- const path = join18(appRoot, relative2);
3502
- 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.`);
3503
3704
  const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
3504
3705
  let resource;
3505
3706
  if (options2.resource) {
@@ -3508,27 +3709,27 @@ async function makeSeed(rawName, options2 = {}) {
3508
3709
  } else {
3509
3710
  resource = resources.find((r) => [kebabCase3(r.name), r.slug].includes(name));
3510
3711
  }
3511
- mkdirSync10(join18(appRoot, SEEDS_DIR), { recursive: true });
3512
- writeFileSync12(path, renderSeed(resource));
3513
- 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}`);
3514
3715
  log(`
3515
- Run it with ${pc14.bold(`flare seed ${name}`)}.`);
3716
+ Run it with ${pc15.bold(`flare seed ${name}`)}.`);
3516
3717
  return relative2;
3517
3718
  }
3518
3719
 
3519
3720
  // src/commands/billing-sync.ts
3520
3721
  import { createRequire as createRequire2 } from "module";
3521
- 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";
3522
3723
  import { tmpdir as tmpdir2 } from "os";
3523
- import { join as join19 } from "path";
3724
+ import { join as join20 } from "path";
3524
3725
  import { pathToFileURL as pathToFileURL2 } from "url";
3525
3726
  import { kebabCase as kebabCase4 } from "@flaredev/core";
3526
- import pc15 from "picocolors";
3727
+ import pc16 from "picocolors";
3527
3728
  function stripeKeys(appRoot) {
3528
3729
  const map = {};
3529
- const path = join19(appRoot, ".dev.vars");
3530
- if (existsSync20(path)) {
3531
- 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/)) {
3532
3733
  const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
3533
3734
  if (match) map[match[1]] = match[2].trim();
3534
3735
  }
@@ -3611,10 +3812,10 @@ async function syncPlans(options2 = {}) {
3611
3812
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
3612
3813
  const db = readD1Databases(appRoot)[0];
3613
3814
  if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
3614
- const appRequire = createRequire2(join19(appRoot, "package.json"));
3815
+ const appRequire = createRequire2(join20(appRoot, "package.json"));
3615
3816
  const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
3616
3817
  const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
3617
- 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";
3618
3819
  const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
3619
3820
  const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
3620
3821
  if (products.length === 0) {
@@ -3635,10 +3836,10 @@ async function syncPlans(options2 = {}) {
3635
3836
  ${existingResult.output.trim()}`);
3636
3837
  }
3637
3838
  const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
3638
- const dir = mkdtempSync2(join19(tmpdir2(), "flare-sync-"));
3839
+ const dir = mkdtempSync2(join20(tmpdir2(), "flare-sync-"));
3639
3840
  try {
3640
- const file = join19(dir, "sync-plans.sql");
3641
- writeFileSync13(file, renderSyncSql(rows));
3841
+ const file = join20(dir, "sync-plans.sql");
3842
+ writeFileSync14(file, renderSyncSql(rows));
3642
3843
  const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
3643
3844
  if (result.code !== 0) throw new Error(`Could not write plans:
3644
3845
  ${result.output.trim()}`);
@@ -3650,26 +3851,26 @@ ${result.output.trim()}`);
3650
3851
  const isNew = !known.has(row.stripePriceId);
3651
3852
  if (isNew) created++;
3652
3853
  const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
3653
- 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)}`);
3654
3855
  }
3655
- 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`));
3656
3857
  const configuration = await syncPortalConfiguration(stripe, rows, appName);
3657
- if (configuration) log(`${pc15.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
3658
- log(pc15.green(`
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(`
3659
3860
  ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
3660
3861
  return { created, updated: rows.length - created, skipped };
3661
3862
  }
3662
3863
 
3663
3864
  // src/commands/sync.ts
3664
- import { existsSync as existsSync21 } from "fs";
3665
- import { join as join20 } from "path";
3666
- import pc16 from "picocolors";
3865
+ import { existsSync as existsSync22 } from "fs";
3866
+ import { join as join21 } from "path";
3867
+ import pc17 from "picocolors";
3667
3868
  async function syncTypes(options2 = {}) {
3668
3869
  const log = options2.log ?? ((message) => console.log(message));
3669
3870
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
3670
3871
  const all = await loadResources(appRoot);
3671
3872
  const policies = await loadPolicies(appRoot);
3672
- const tracksPolicies = policies.length > 0 || existsSync21(join20(appRoot, "policies/index.ts"));
3873
+ const tracksPolicies = policies.length > 0 || existsSync22(join21(appRoot, "policies/index.ts"));
3673
3874
  const plan = planFiles(all, tracksPolicies ? policies : void 0);
3674
3875
  if (!options2.check) ensureSupportFiles(appRoot, log);
3675
3876
  const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
@@ -3681,42 +3882,42 @@ async function syncTypes(options2 = {}) {
3681
3882
  if (drift.length) {
3682
3883
  log(
3683
3884
  `
3684
- ${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"}.
3685
3886
  Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
3686
3887
  );
3687
3888
  }
3688
3889
  if (unmarked.length) {
3689
3890
  log(`
3690
- ${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.`);
3691
3892
  }
3692
3893
  if (orphans.length) {
3693
3894
  const resources = [...new Set(orphans.map((o) => o.resource))];
3694
3895
  log(`
3695
- ${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(", ")}):`);
3696
3897
  for (const orphan of orphans) log(` ${orphan.path}`);
3697
3898
  log(
3698
- `If the resource was deleted, remove them with ${pc16.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
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.`
3699
3900
  );
3700
3901
  }
3701
3902
  const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
3702
3903
  if (tablesChanged && !options2.check) {
3703
3904
  log(`
3704
- ${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")}.`);
3705
3906
  }
3706
3907
  const problems = drift.length + unmarked.length + orphans.length;
3707
3908
  if (options2.check) {
3708
3909
  const stale = changed.length;
3709
- 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."));
3710
3911
  else log(`
3711
- ${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")}.`);
3712
3913
  return stale + problems > 0 ? 1 : 0;
3713
3914
  }
3714
- 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."));
3715
3916
  return problems > 0 ? 1 : 0;
3716
3917
  }
3717
3918
 
3718
3919
  // src/commands/user-role.ts
3719
- import pc17 from "picocolors";
3920
+ import pc18 from "picocolors";
3720
3921
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
3721
3922
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
3722
3923
  function userRoleSql(email, role) {
@@ -3730,8 +3931,8 @@ async function setUserRole(email, role, options2 = {}) {
3730
3931
  const sql2 = userRoleSql(email, role);
3731
3932
  const roles = await listRoles(options2);
3732
3933
  if (roles && !roles.includes(role)) {
3733
- log(pc17.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
3734
- log(pc17.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
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" : ""}`));
3735
3936
  return 1;
3736
3937
  }
3737
3938
  const db = readD1Databases(appRoot)[0];
@@ -3745,22 +3946,24 @@ ${result.output.trim()}`);
3745
3946
  const [first] = extractJson(result.output);
3746
3947
  const updated = first?.results ?? [];
3747
3948
  if (updated.length === 0) {
3748
- 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.`));
3749
3950
  return 1;
3750
3951
  }
3751
- 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"}).`));
3752
3953
  return 0;
3753
3954
  }
3754
3955
 
3755
3956
  // src/cli.ts
3756
3957
  function createCli() {
3757
3958
  const cli = cac("flare");
3758
- 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) => {
3759
3960
  const install = !options2.skipInstall;
3760
- 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 });
3761
3964
  printNextSteps(result, install);
3762
3965
  });
3763
- 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)"').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(
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(
3764
3967
  async (generator, name, options2) => {
3765
3968
  if (generator === "billing")
3766
3969
  return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
@@ -3802,6 +4005,7 @@ function createCli() {
3802
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) => {
3803
4006
  process.exitCode = await syncTypes(options2);
3804
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));
3805
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) => {
3806
4010
  const local = toLocalUrl(target);
3807
4011
  const tunnel = await openTunnel(local);
@@ -3837,6 +4041,6 @@ try {
3837
4041
  }
3838
4042
  }
3839
4043
  } catch (error) {
3840
- 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)}`));
3841
4045
  process.exitCode = 1;
3842
4046
  }