betterstart-cli 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3068,20 +3068,26 @@ async function resolvePasswordEnvValue(options) {
3068
3068
  options.overwriteEnvKeys.add(options.key);
3069
3069
  return result.trim();
3070
3070
  }
3071
- async function collectResendConfig(cwd) {
3071
+ async function collectResendConfig(cwd, presetApiKey) {
3072
3072
  const overwriteEnvKeys = /* @__PURE__ */ new Set();
3073
- const apiKey = await resolvePasswordEnvValue({
3074
- cwd,
3075
- key: "BETTERSTART_RESEND_API_KEY",
3076
- message: "Resend API key",
3077
- cancelMessage: "Resend configuration cancelled.",
3078
- overwriteEnvKeys,
3079
- validate(value) {
3080
- if (!value?.trim()) {
3081
- return "Enter a Resend API key.";
3073
+ let apiKey;
3074
+ if (presetApiKey?.trim()) {
3075
+ apiKey = presetApiKey.trim();
3076
+ overwriteEnvKeys.add("BETTERSTART_RESEND_API_KEY");
3077
+ } else {
3078
+ apiKey = await resolvePasswordEnvValue({
3079
+ cwd,
3080
+ key: "BETTERSTART_RESEND_API_KEY",
3081
+ message: "Resend API key",
3082
+ cancelMessage: "Resend configuration cancelled.",
3083
+ overwriteEnvKeys,
3084
+ validate(value) {
3085
+ if (!value?.trim()) {
3086
+ return "Enter a Resend API key.";
3087
+ }
3082
3088
  }
3083
- }
3084
- });
3089
+ });
3090
+ }
3085
3091
  const fromAddress = await resolveTextEnvValue({
3086
3092
  cwd,
3087
3093
  key: "BETTERSTART_EMAIL_FROM",
@@ -3197,12 +3203,12 @@ async function promptR2Config(cwd) {
3197
3203
  updatedEnvKeys: envResult.updated
3198
3204
  };
3199
3205
  }
3200
- async function collectIntegrationConfig(cwd, integrationIds) {
3206
+ async function collectIntegrationConfig(cwd, integrationIds, options) {
3201
3207
  const sections = [];
3202
3208
  const overwriteKeys = /* @__PURE__ */ new Set();
3203
3209
  for (const integrationId of resolveIntegrationInstallOrder(integrationIds)) {
3204
3210
  const definition = getIntegrationDefinition(integrationId);
3205
- const collected = definition.configure === "resend" ? await collectResendConfig(cwd) : definition.configure === "r2" ? await collectR2Config(cwd) : void 0;
3211
+ const collected = definition.configure === "resend" ? await collectResendConfig(cwd, options?.resendApiKey) : definition.configure === "r2" ? await collectR2Config(cwd) : void 0;
3206
3212
  if (!collected) {
3207
3213
  continue;
3208
3214
  }
@@ -5283,14 +5289,14 @@ function buildStepsConstant(steps) {
5283
5289
  ${entries.join(",\n")}
5284
5290
  ]`;
5285
5291
  }
5286
- function buildComponentSource(p20) {
5292
+ function buildComponentSource(p21) {
5287
5293
  const providerOpen = ` <NuqsAdapter>
5288
5294
  <React.Suspense fallback={null}>
5289
- <${p20.pascal}FormInner />
5295
+ <${p21.pascal}FormInner />
5290
5296
  </React.Suspense>
5291
5297
  </NuqsAdapter>`;
5292
5298
  const exportWrapper = `
5293
- export function ${p20.pascal}Form() {
5299
+ export function ${p21.pascal}Form() {
5294
5300
  return (
5295
5301
  ${providerOpen}
5296
5302
  )
@@ -5299,22 +5305,22 @@ ${providerOpen}
5299
5305
  return `'use client'
5300
5306
 
5301
5307
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
5302
- import { ChevronLeft, ChevronRight${p20.hasListFields ? ", Trash2" : ""} } from 'lucide-react'
5308
+ import { ChevronLeft, ChevronRight${p21.hasListFields ? ", Trash2" : ""} } from 'lucide-react'
5303
5309
  import { createParser, useQueryState } from 'nuqs'
5304
5310
  import { NuqsAdapter } from 'nuqs/adapters/next/app'
5305
5311
  import * as React from 'react'
5306
- ${p20.rhfImport}
5312
+ ${p21.rhfImport}
5307
5313
  import { z } from 'zod/v3'
5308
- import { create${p20.pascal}Submission } from '@admin/actions/${p20.actionImportPath}'
5314
+ import { create${p21.pascal}Submission } from '@admin/actions/${p21.actionImportPath}'
5309
5315
 
5310
5316
  const formSchema = z.object({
5311
- ${p20.zodFields}
5317
+ ${p21.zodFields}
5312
5318
  })
5313
5319
 
5314
5320
  type FormValues = z.infer<typeof formSchema>
5315
5321
  ${buildFieldErrorHelper()}
5316
5322
 
5317
- ${p20.stepsConst}
5323
+ ${p21.stepsConst}
5318
5324
 
5319
5325
  const stepParser = createParser({
5320
5326
  parse(value) {
@@ -5332,7 +5338,7 @@ const stepParser = createParser({
5332
5338
  }
5333
5339
  }).withDefault(0)
5334
5340
 
5335
- function ${p20.pascal}FormInner() {
5341
+ function ${p21.pascal}FormInner() {
5336
5342
  const [currentStep, setCurrentStep] = useQueryState('step', stepParser)
5337
5343
  const [submitted, setSubmitted] = React.useState(false)
5338
5344
  const [submitting, startSubmitTransition] = React.useTransition()
@@ -5340,11 +5346,11 @@ function ${p20.pascal}FormInner() {
5340
5346
  const form = useForm<FormValues>({
5341
5347
  resolver: standardSchemaResolver(formSchema),
5342
5348
  defaultValues: {
5343
- ${p20.defaults}
5349
+ ${p21.defaults}
5344
5350
  },
5345
5351
  })
5346
5352
 
5347
- ${p20.fieldArraySetup}${p20.watchSetup}
5353
+ ${p21.fieldArraySetup}${p21.watchSetup}
5348
5354
  async function handleNext() {
5349
5355
  const stepFields = STEPS[currentStep].fields as (keyof FormValues)[]
5350
5356
  const isValid = await form.trigger(stepFields, { shouldFocus: true })
@@ -5360,9 +5366,9 @@ ${p20.fieldArraySetup}${p20.watchSetup}
5360
5366
  function onSubmit(values: FormValues) {
5361
5367
  startSubmitTransition(async () => {
5362
5368
  try {
5363
- const result = await create${p20.pascal}Submission(values)
5369
+ const result = await create${p21.pascal}Submission(values)
5364
5370
  if (result.success) {
5365
- ${p20.successHandler}
5371
+ ${p21.successHandler}
5366
5372
  } else {
5367
5373
  form.setError('root', { message: result.error || 'Something went wrong' })
5368
5374
  }
@@ -5376,7 +5382,7 @@ ${p20.fieldArraySetup}${p20.watchSetup}
5376
5382
  return (
5377
5383
  <div className="rounded-sm border p-6 text-center">
5378
5384
  <h3 className="text-lg font-semibold">Thank you!</h3>
5379
- <p className="mt-2 text-muted-foreground">${p20.successMessage}</p>
5385
+ <p className="mt-2 text-muted-foreground">${p21.successMessage}</p>
5380
5386
  </div>
5381
5387
  )
5382
5388
  }
@@ -5393,7 +5399,7 @@ ${p20.fieldArraySetup}${p20.watchSetup}
5393
5399
 
5394
5400
  {/* Step content */}
5395
5401
  <div key={currentStep} className="animate-in fade-in duration-300 space-y-6">
5396
- ${p20.stepContentBlocks}
5402
+ ${p21.stepContentBlocks}
5397
5403
  </div>
5398
5404
 
5399
5405
  {form.formState.errors.root && (
@@ -5427,7 +5433,7 @@ ${p20.stepContentBlocks}
5427
5433
  onClick={() => form.handleSubmit(onSubmit)()}
5428
5434
  className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-xs transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
5429
5435
  >
5430
- {submitting ? 'Submitting...' : ${p20.submitText}}
5436
+ {submitting ? 'Submitting...' : ${p21.submitText}}
5431
5437
  </button>
5432
5438
  )}
5433
5439
  </div>
@@ -21743,38 +21749,44 @@ ${validationErrors.map((error) => ` - ${error}`).join("\n")}`
21743
21749
  import { execFileSync as execFileSync5, spawn as spawn4 } from "child_process";
21744
21750
  import fs37 from "fs";
21745
21751
  import path48 from "path";
21746
- import * as p15 from "@clack/prompts";
21752
+ import * as p16 from "@clack/prompts";
21747
21753
 
21748
21754
  // adapters/next/init/prompts/database.ts
21749
21755
  import { execFileSync as execFileSync4 } from "child_process";
21756
+ import { styleText } from "util";
21750
21757
  import * as p9 from "@clack/prompts";
21751
21758
  import pc from "picocolors";
21752
21759
  var VERCEL_NEON_URL = "https://vercel.com/dashboard/integrations/checkout/neon";
21753
- async function promptDatabase(options) {
21760
+ async function promptServices(options) {
21754
21761
  if (!options.allowVercel) {
21755
21762
  const url2 = await promptConnectionString();
21756
21763
  return { provider: "manual", url: url2 };
21757
21764
  }
21758
21765
  const choice = await p9.select({
21759
- message: "How would you like to connect your PostgreSQL database?",
21766
+ message: `How would you like to connect your services?
21767
+ ${styleText(
21768
+ "dim",
21769
+ "Database, Email, and File Storage"
21770
+ )}`,
21760
21771
  options: [
21761
21772
  {
21762
- value: "vercel-neon",
21763
- label: "Vercel (Neon)",
21764
- hint: "create & connect a free Postgres database via the Vercel CLI"
21773
+ value: "vercel",
21774
+ label: "Vercel",
21775
+ hint: "provision Neon Postgres + Resend email via the Vercel CLI"
21765
21776
  },
21766
21777
  {
21767
21778
  value: "manual",
21768
- label: "Enter connection string manually"
21779
+ label: "Manual",
21780
+ hint: "enter connection details yourself"
21769
21781
  }
21770
21782
  ],
21771
- initialValue: "vercel-neon"
21783
+ initialValue: "vercel"
21772
21784
  });
21773
21785
  if (p9.isCancel(choice)) {
21774
21786
  p9.cancel("Setup cancelled.");
21775
21787
  process.exit(0);
21776
21788
  }
21777
- if (choice === "vercel-neon") {
21789
+ if (choice === "vercel") {
21778
21790
  return { provider: "vercel-cli" };
21779
21791
  }
21780
21792
  const url = await promptConnectionString();
@@ -21822,9 +21834,9 @@ function openBrowser(url) {
21822
21834
  }
21823
21835
 
21824
21836
  // adapters/next/init/prompts/plugins.ts
21825
- import { styleText } from "util";
21837
+ import { styleText as styleText2 } from "util";
21826
21838
  import * as p10 from "@clack/prompts";
21827
- async function promptPlugins(cwd) {
21839
+ async function promptPlugins(cwd, options) {
21828
21840
  const sections = [];
21829
21841
  const overwriteKeys = /* @__PURE__ */ new Set();
21830
21842
  const mergeIntegrationConfig = (collected) => {
@@ -21835,7 +21847,7 @@ async function promptPlugins(cwd) {
21835
21847
  };
21836
21848
  const selectedPlugins = await p10.multiselect({
21837
21849
  message: `Select presets
21838
- ${styleText("dim", "Press [Spacebar] to select/unselect")}`,
21850
+ ${styleText2("dim", "Press [Spacebar] to select/unselect")}`,
21839
21851
  options: [
21840
21852
  {
21841
21853
  value: "blog",
@@ -21873,37 +21885,47 @@ ${styleText("dim", "Press [Spacebar] to select/unselect")}`,
21873
21885
  if (storage === "r2") {
21874
21886
  mergeIntegrationConfig(await collectIntegrationConfig(cwd, ["r2"]));
21875
21887
  }
21876
- const selectedEmailProvider = await p10.select({
21877
- message: `Which email provider do you want to set up?
21878
- ${styleText(
21879
- "dim",
21880
- "Auth + Forms email delivery"
21881
- )}`,
21882
- options: [
21883
- {
21884
- value: "resend",
21885
- label: "Resend"
21886
- },
21887
- {
21888
- value: "skip",
21889
- label: "Skip"
21890
- },
21891
- {
21892
- value: "cloudflare",
21893
- label: `Cloudflare ${styleText("dim", "(coming soon)")}`,
21894
- disabled: true
21895
- }
21896
- ],
21897
- initialValue: "resend"
21898
- });
21899
- if (p10.isCancel(selectedEmailProvider)) {
21900
- p10.cancel("Setup cancelled.");
21901
- process.exit(0);
21902
- }
21903
21888
  const integrations = [];
21904
- if (selectedEmailProvider === "resend") {
21889
+ if (options?.vercelResendApiKey) {
21890
+ p10.log.info(`Email provider: Resend ${styleText2("dim", "(connected via Vercel)")}`);
21905
21891
  integrations.push("resend");
21906
- mergeIntegrationConfig(await collectIntegrationConfig(cwd, ["resend"]));
21892
+ mergeIntegrationConfig(
21893
+ await collectIntegrationConfig(cwd, ["resend"], {
21894
+ resendApiKey: options.vercelResendApiKey
21895
+ })
21896
+ );
21897
+ } else {
21898
+ const selectedEmailProvider = await p10.select({
21899
+ message: `Which email provider do you want to set up?
21900
+ ${styleText2(
21901
+ "dim",
21902
+ "Auth + Forms email delivery"
21903
+ )}`,
21904
+ options: [
21905
+ {
21906
+ value: "resend",
21907
+ label: "Resend"
21908
+ },
21909
+ {
21910
+ value: "skip",
21911
+ label: "Skip"
21912
+ },
21913
+ {
21914
+ value: "cloudflare",
21915
+ label: `Cloudflare ${styleText2("dim", "(coming soon)")}`,
21916
+ disabled: true
21917
+ }
21918
+ ],
21919
+ initialValue: "resend"
21920
+ });
21921
+ if (p10.isCancel(selectedEmailProvider)) {
21922
+ p10.cancel("Setup cancelled.");
21923
+ process.exit(0);
21924
+ }
21925
+ if (selectedEmailProvider === "resend") {
21926
+ integrations.push("resend");
21927
+ mergeIntegrationConfig(await collectIntegrationConfig(cwd, ["resend"]));
21928
+ }
21907
21929
  }
21908
21930
  if (storage === "r2") {
21909
21931
  integrations.push("r2");
@@ -23827,7 +23849,7 @@ function scaffoldTsconfig(cwd, config) {
23827
23849
  }
23828
23850
 
23829
23851
  // adapters/next/init/vercel/flow.ts
23830
- import * as p14 from "@clack/prompts";
23852
+ import * as p15 from "@clack/prompts";
23831
23853
  import pc4 from "picocolors";
23832
23854
 
23833
23855
  // adapters/next/init/vercel/auth.ts
@@ -23956,20 +23978,20 @@ async function checkVercelAuth(runner, cwd, env) {
23956
23978
  }
23957
23979
  async function ensureVercelAuth(runner, cwd, options) {
23958
23980
  const env = options.env ?? process.env;
23959
- const spinner9 = p12.spinner();
23960
- spinner9.start(
23981
+ const spinner10 = p12.spinner();
23982
+ spinner10.start(
23961
23983
  runner.source === "npx" ? "Checking Vercel sign-in (first run downloads the Vercel CLI)" : "Checking Vercel sign-in"
23962
23984
  );
23963
23985
  const existing = await checkVercelAuth(runner, cwd, env);
23964
23986
  if (existing.authed) {
23965
- spinner9.stop(signedInMessage(existing.username));
23987
+ spinner10.stop(signedInMessage(existing.username));
23966
23988
  return existing;
23967
23989
  }
23968
23990
  if (env.VERCEL_TOKEN) {
23969
- spinner9.stop("Could not verify VERCEL_TOKEN with Vercel");
23991
+ spinner10.stop("Could not verify VERCEL_TOKEN with Vercel");
23970
23992
  return { authed: false, reason: "login-failed" };
23971
23993
  }
23972
- spinner9.stop("Not signed in to Vercel yet");
23994
+ spinner10.stop("Not signed in to Vercel yet");
23973
23995
  p12.log.info(
23974
23996
  `Sign in to Vercel to continue \u2014 a browser link and code will appear below. ${pc3.dim(
23975
23997
  "(or press Ctrl-C to enter a connection string manually)"
@@ -23999,13 +24021,48 @@ function signedInMessage(username) {
23999
24021
  }
24000
24022
 
24001
24023
  // adapters/next/init/vercel/neon.ts
24024
+ import * as p13 from "@clack/prompts";
24025
+
24026
+ // adapters/next/init/vercel/env-pull.ts
24002
24027
  import fs34 from "fs";
24003
24028
  import os from "os";
24004
24029
  import path45 from "path";
24005
- import * as p13 from "@clack/prompts";
24030
+ var ENV_PULL_TIMEOUT_MS = 6e4;
24031
+ async function pullVercelEnvValue(runner, cwd, read, env) {
24032
+ const tmpDir = fs34.mkdtempSync(path45.join(os.tmpdir(), "betterstart-vercel-"));
24033
+ const tmpEnv = path45.join(tmpDir, ".env.pull");
24034
+ try {
24035
+ const pull = await runVercel(
24036
+ runner,
24037
+ ["env", "pull", tmpEnv, "--environment", "development", "--yes"],
24038
+ { cwd, mode: "capture", timeoutMs: ENV_PULL_TIMEOUT_MS, env }
24039
+ );
24040
+ if (!pull.success) return void 0;
24041
+ return read(tmpEnv);
24042
+ } finally {
24043
+ try {
24044
+ fs34.rmSync(tmpDir, { recursive: true, force: true });
24045
+ } catch {
24046
+ }
24047
+ }
24048
+ }
24049
+ function parseDotenvFile(filePath) {
24050
+ const vars = /* @__PURE__ */ new Map();
24051
+ if (!fs34.existsSync(filePath)) return vars;
24052
+ for (const line of fs34.readFileSync(filePath, "utf-8").split("\n")) {
24053
+ const trimmed = line.trim();
24054
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
24055
+ const eq = trimmed.indexOf("=");
24056
+ const key = trimmed.slice(0, eq).trim();
24057
+ const value = trimmed.slice(eq + 1).replace(/^['"]|['"]$/g, "").trim();
24058
+ if (key) vars.set(key, value);
24059
+ }
24060
+ return vars;
24061
+ }
24062
+
24063
+ // adapters/next/init/vercel/neon.ts
24006
24064
  var PROVISION_TIMEOUT_MS = 18e4;
24007
24065
  var INTERACTIVE_PROVISION_TIMEOUT_MS = 6e5;
24008
- var ENV_PULL_TIMEOUT_MS = 6e4;
24009
24066
  async function provisionNeonInteractive(runner, cwd, options) {
24010
24067
  const addArgs = ["integration", "add", "neon", "--no-env-pull"];
24011
24068
  if (options.plan) addArgs.push("--plan", options.plan);
@@ -24031,8 +24088,8 @@ async function provisionNeonInteractive(runner, cwd, options) {
24031
24088
  async function provisionNeon(runner, cwd, options) {
24032
24089
  const addArgs = ["integration", "add", "neon", "--no-env-pull", "--format", "json"];
24033
24090
  if (options.plan) addArgs.push("--plan", options.plan);
24034
- const spinner9 = p13.spinner();
24035
- spinner9.start("Provisioning Neon Postgres via Vercel");
24091
+ const spinner10 = p13.spinner();
24092
+ spinner10.start("Provisioning Neon Postgres via Vercel");
24036
24093
  const add = await runVercel(runner, addArgs, {
24037
24094
  cwd,
24038
24095
  mode: "capture",
@@ -24040,7 +24097,7 @@ async function provisionNeon(runner, cwd, options) {
24040
24097
  env: options.env
24041
24098
  });
24042
24099
  if (!add.success) {
24043
- spinner9.stop("Neon provisioning did not complete");
24100
+ spinner10.stop("Neon provisioning did not complete");
24044
24101
  const combined = `${add.stdout}
24045
24102
  ${add.stderr}`.trim();
24046
24103
  const detail = combined || void 0;
@@ -24050,44 +24107,20 @@ ${add.stderr}`.trim();
24050
24107
  }
24051
24108
  const meta = parseVercelJson(add.stdout);
24052
24109
  const resourceName = meta?.resourceName ?? meta?.name;
24053
- spinner9.message("Retrieving the database connection string");
24110
+ spinner10.message("Retrieving the database connection string");
24054
24111
  const databaseUrl = await pullDatabaseUrl(runner, cwd, options.env);
24055
24112
  if (!databaseUrl) {
24056
- spinner9.stop("No DATABASE_URL came back from Vercel");
24113
+ spinner10.stop("No DATABASE_URL came back from Vercel");
24057
24114
  return { resourceName, failure: "env-pull-empty" };
24058
24115
  }
24059
- spinner9.stop("Captured the Neon DATABASE_URL");
24116
+ spinner10.stop("Captured the Neon DATABASE_URL");
24060
24117
  return { databaseUrl, resourceName };
24061
24118
  }
24062
- async function pullDatabaseUrl(runner, cwd, env) {
24063
- const tmpDir = fs34.mkdtempSync(path45.join(os.tmpdir(), "betterstart-vercel-"));
24064
- const tmpEnv = path45.join(tmpDir, ".env.pull");
24065
- try {
24066
- const pull = await runVercel(
24067
- runner,
24068
- ["env", "pull", tmpEnv, "--environment", "development", "--yes"],
24069
- { cwd, mode: "capture", timeoutMs: ENV_PULL_TIMEOUT_MS, env }
24070
- );
24071
- if (!pull.success) return void 0;
24072
- return readDbUrlFromDotenv(tmpEnv);
24073
- } finally {
24074
- try {
24075
- fs34.rmSync(tmpDir, { recursive: true, force: true });
24076
- } catch {
24077
- }
24078
- }
24119
+ function pullDatabaseUrl(runner, cwd, env) {
24120
+ return pullVercelEnvValue(runner, cwd, readDbUrlFromDotenv, env);
24079
24121
  }
24080
24122
  function readDbUrlFromDotenv(filePath) {
24081
- if (!fs34.existsSync(filePath)) return void 0;
24082
- const vars = /* @__PURE__ */ new Map();
24083
- for (const line of fs34.readFileSync(filePath, "utf-8").split("\n")) {
24084
- const trimmed = line.trim();
24085
- if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
24086
- const eq = trimmed.indexOf("=");
24087
- const key = trimmed.slice(0, eq).trim();
24088
- const value = trimmed.slice(eq + 1).replace(/^['"]|['"]$/g, "").trim();
24089
- if (key) vars.set(key, value);
24090
- }
24123
+ const vars = parseDotenvFile(filePath);
24091
24124
  const isPg = (v) => !!v && (v.startsWith("postgres://") || v.startsWith("postgresql://"));
24092
24125
  if (isPg(vars.get("DATABASE_URL"))) return vars.get("DATABASE_URL");
24093
24126
  if (isPg(vars.get("POSTGRES_URL"))) return vars.get("POSTGRES_URL");
@@ -24163,11 +24196,47 @@ function readLinkedProjectId(cwd) {
24163
24196
  }
24164
24197
  }
24165
24198
 
24199
+ // adapters/next/init/vercel/resend.ts
24200
+ import * as p14 from "@clack/prompts";
24201
+ var INTERACTIVE_PROVISION_TIMEOUT_MS2 = 6e5;
24202
+ async function provisionResendInteractive(runner, cwd, options) {
24203
+ const add = await runVercel(runner, ["integration", "add", "resend", "--no-env-pull"], {
24204
+ cwd,
24205
+ mode: "inherit",
24206
+ timeoutMs: INTERACTIVE_PROVISION_TIMEOUT_MS2,
24207
+ env: options.env
24208
+ });
24209
+ if (!add.success) {
24210
+ return { failure: add.timedOut ? "timeout" : "provision-failed" };
24211
+ }
24212
+ const pullSpinner = p14.spinner();
24213
+ pullSpinner.start("Retrieving the Resend API key");
24214
+ const apiKey = await pullVercelEnvValue(runner, cwd, readResendApiKeyFromDotenv, options.env);
24215
+ if (!apiKey) {
24216
+ pullSpinner.stop("No RESEND_API_KEY came back from Vercel");
24217
+ return { failure: "env-pull-empty" };
24218
+ }
24219
+ pullSpinner.stop("Captured the Resend API key");
24220
+ return { apiKey };
24221
+ }
24222
+ function readResendApiKeyFromDotenv(filePath) {
24223
+ const vars = parseDotenvFile(filePath);
24224
+ const direct = vars.get("RESEND_API_KEY");
24225
+ if (direct) return direct;
24226
+ for (const [key, value] of vars) {
24227
+ if (/RESEND_API_KEY$/.test(key) && value) return value;
24228
+ }
24229
+ for (const value of vars.values()) {
24230
+ if (value.startsWith("re_")) return value;
24231
+ }
24232
+ return void 0;
24233
+ }
24234
+
24166
24235
  // adapters/next/init/vercel/flow.ts
24167
24236
  async function runVercelNeonFlow(options) {
24168
24237
  const env = options.env ?? process.env;
24169
24238
  try {
24170
- const runnerSpinner = p14.spinner();
24239
+ const runnerSpinner = p15.spinner();
24171
24240
  runnerSpinner.start("Checking for the Vercel CLI");
24172
24241
  const runner = await resolveVercelRunner();
24173
24242
  runnerSpinner.stop(
@@ -24175,10 +24244,10 @@ async function runVercelNeonFlow(options) {
24175
24244
  );
24176
24245
  const auth = await ensureVercelAuth(runner, options.cwd, { env });
24177
24246
  if (!auth.authed) {
24178
- p14.log.warn(authFailureMessage(auth.reason));
24247
+ p15.log.warn(authFailureMessage(auth.reason));
24179
24248
  return { ok: false };
24180
24249
  }
24181
- const projectSpinner = p14.spinner();
24250
+ const projectSpinner = p15.spinner();
24182
24251
  projectSpinner.start(`Creating Vercel project ${pc4.cyan(options.projectName)}`);
24183
24252
  const project2 = await createVercelProject(runner, options.cwd, options.projectName, env);
24184
24253
  projectSpinner.stop(
@@ -24186,19 +24255,33 @@ async function runVercelNeonFlow(options) {
24186
24255
  );
24187
24256
  const neon = await provisionNeonForMode(runner, options);
24188
24257
  if (neon.failure || !neon.databaseUrl) {
24189
- p14.log.warn(neonFailureMessage(neon.failure));
24190
- if (neon.detail) p14.log.message(pc4.dim(neon.detail));
24258
+ p15.log.warn(neonFailureMessage(neon.failure));
24259
+ if (neon.detail) p15.log.message(pc4.dim(neon.detail));
24191
24260
  return { ok: false };
24192
24261
  }
24193
- p14.log.success(`Provisioned Neon Postgres for ${pc4.cyan(project2.name)}`);
24262
+ p15.log.success(`Provisioned Neon Postgres for ${pc4.cyan(project2.name)}`);
24263
+ let resendApiKey;
24264
+ if (options.provisionResend && options.interactive) {
24265
+ p15.log.info(
24266
+ `Set up Resend email in the Vercel prompts below ${pc4.dim("(the Free plan is recommended).")}`
24267
+ );
24268
+ const resend = await provisionResendInteractive(runner, options.cwd, { env });
24269
+ if (resend.apiKey) {
24270
+ resendApiKey = resend.apiKey;
24271
+ p15.log.success(`Provisioned Resend email for ${pc4.cyan(project2.name)}`);
24272
+ } else {
24273
+ p15.log.warn(resendFailureMessage(resend.failure));
24274
+ }
24275
+ }
24194
24276
  return {
24195
24277
  ok: true,
24196
24278
  databaseUrl: neon.databaseUrl,
24197
24279
  vercelProjectId: project2.projectId,
24198
- neonResourceName: neon.resourceName
24280
+ neonResourceName: neon.resourceName,
24281
+ resendApiKey
24199
24282
  };
24200
24283
  } catch (error) {
24201
- p14.log.warn(
24284
+ p15.log.warn(
24202
24285
  `Vercel provisioning failed: ${error instanceof Error ? error.message : String(error)}`
24203
24286
  );
24204
24287
  return { ok: false };
@@ -24207,7 +24290,7 @@ async function runVercelNeonFlow(options) {
24207
24290
  function provisionNeonForMode(runner, options) {
24208
24291
  const opts = { plan: options.plan, env: options.env };
24209
24292
  if (options.interactive) {
24210
- p14.log.info(
24293
+ p15.log.info(
24211
24294
  `Create your Neon database in the Vercel prompts below ${pc4.dim("(the Free plan is recommended).")}`
24212
24295
  );
24213
24296
  return provisionNeonInteractive(runner, options.cwd, opts);
@@ -24228,6 +24311,16 @@ function authFailureMessage(reason) {
24228
24311
  return "Could not sign in to Vercel.";
24229
24312
  }
24230
24313
  }
24314
+ function resendFailureMessage(reason) {
24315
+ switch (reason) {
24316
+ case "env-pull-empty":
24317
+ return "Provisioned Resend, but no RESEND_API_KEY came back \u2014 you can pick an email provider next.";
24318
+ case "timeout":
24319
+ return "Resend provisioning timed out \u2014 you can pick an email provider next.";
24320
+ default:
24321
+ return "Resend provisioning did not complete \u2014 you can pick an email provider next.";
24322
+ }
24323
+ }
24231
24324
  function neonFailureMessage(reason) {
24232
24325
  switch (reason) {
24233
24326
  case "terms-required":
@@ -24490,13 +24583,13 @@ async function runSeedCommand(options) {
24490
24583
  }
24491
24584
  );
24492
24585
  });
24493
- const spinner9 = clack.spinner();
24494
- spinner9.start("Creating admin user...");
24586
+ const spinner10 = clack.spinner();
24587
+ spinner10.start("Creating admin user...");
24495
24588
  try {
24496
24589
  const result = await runSeed2(false);
24497
24590
  if (result.code === 2) {
24498
24591
  const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
24499
- spinner9.stop(`Account already exists for ${email}`);
24592
+ spinner10.stop(`Account already exists for ${email}`);
24500
24593
  const overwrite = await clack.confirm({
24501
24594
  message: `An admin account (${existingName}) already exists with this email. Replace it?`
24502
24595
  });
@@ -24508,14 +24601,14 @@ async function runSeedCommand(options) {
24508
24601
  }
24509
24602
  return;
24510
24603
  }
24511
- spinner9.start("Replacing admin user...");
24604
+ spinner10.start("Replacing admin user...");
24512
24605
  await runSeed2(true);
24513
- spinner9.stop("Admin user replaced");
24606
+ spinner10.stop("Admin user replaced");
24514
24607
  } else {
24515
- spinner9.stop("Admin user created");
24608
+ spinner10.stop("Admin user created");
24516
24609
  }
24517
24610
  } catch (err) {
24518
- spinner9.stop("Failed to create admin user");
24611
+ spinner10.stop("Failed to create admin user");
24519
24612
  const errMsg = err instanceof Error ? err.message : String(err);
24520
24613
  clack.log.error(errMsg);
24521
24614
  clack.log.info("You can run the seed script manually:");
@@ -24615,7 +24708,7 @@ function removeExistingAdminPaths(cwd, namespaces) {
24615
24708
  return removed;
24616
24709
  }
24617
24710
  async function runInitCommand(name, options) {
24618
- p15.intro(pc5.bgCyan(pc5.black(" Setup Your Dashboard ")));
24711
+ p16.intro(pc5.bgCyan(pc5.black(" Setup Your Dashboard ")));
24619
24712
  let cwd = process.cwd();
24620
24713
  let projectName = path48.basename(cwd);
24621
24714
  let forceMode = Boolean(options.force);
@@ -24624,13 +24717,13 @@ async function runInitCommand(name, options) {
24624
24717
  try {
24625
24718
  namespace = validateAdminNamespace(options.namespace);
24626
24719
  } catch (error) {
24627
- p15.log.error(error instanceof Error ? error.message : String(error));
24720
+ p16.log.error(error instanceof Error ? error.message : String(error));
24628
24721
  process.exit(1);
24629
24722
  }
24630
24723
  }
24631
24724
  if (!options.yes && !options.namespace) {
24632
24725
  const defaultDashboardPath = resolveAdminNamespace(DEFAULT_ADMIN_NAMESPACE).routePath;
24633
- const namespaceInput = await p15.text({
24726
+ const namespaceInput = await p16.text({
24634
24727
  message: "What's the path you want the dashboard to be?",
24635
24728
  placeholder: `eg. ${defaultDashboardPath}`,
24636
24729
  defaultValue: defaultDashboardPath,
@@ -24643,8 +24736,8 @@ async function runInitCommand(name, options) {
24643
24736
  }
24644
24737
  }
24645
24738
  });
24646
- if (p15.isCancel(namespaceInput)) {
24647
- p15.cancel("Setup cancelled.");
24739
+ if (p16.isCancel(namespaceInput)) {
24740
+ p16.cancel("Setup cancelled.");
24648
24741
  process.exit(0);
24649
24742
  }
24650
24743
  namespace = validateAdminDashboardPath(namespaceInput);
@@ -24654,16 +24747,16 @@ async function runInitCommand(name, options) {
24654
24747
  let isFreshProject = false;
24655
24748
  let srcDir;
24656
24749
  if (project2.isExisting) {
24657
- p15.log.info(`Next.js app detected ${pc5.dim("\xB7")} ${pc5.cyan(pm)}`);
24750
+ p16.log.info(`Next.js app detected ${pc5.dim("\xB7")} ${pc5.cyan(pm)}`);
24658
24751
  srcDir = project2.hasSrcDir;
24659
24752
  if (!project2.hasTypeScript) {
24660
- p15.log.error("TypeScript is required. Please add a tsconfig.json first.");
24753
+ p16.log.error("TypeScript is required. Please add a tsconfig.json first.");
24661
24754
  process.exit(1);
24662
24755
  }
24663
24756
  if (forceMode) {
24664
24757
  const nuked = removeExistingAdminPaths(cwd, await resolveForceInitNamespaces(cwd, namespace));
24665
24758
  if (nuked > 0) {
24666
- p15.log.warn(`${pc5.yellow("Force mode:")} removed ${nuked} existing admin paths`);
24759
+ p16.log.warn(`${pc5.yellow("Force mode:")} removed ${nuked} existing admin paths`);
24667
24760
  }
24668
24761
  project2 = detectProject(cwd, namespace);
24669
24762
  } else if (project2.conflicts.length > 0) {
@@ -24672,9 +24765,9 @@ async function runInitCommand(name, options) {
24672
24765
  "",
24673
24766
  pc5.dim(`Use ${pc5.bold("--force")} to remove existing admin files before scaffolding.`)
24674
24767
  );
24675
- p15.note(conflictLines.join("\n"), pc5.yellow("Conflicts"));
24768
+ p16.note(conflictLines.join("\n"), pc5.yellow("Conflicts"));
24676
24769
  if (!options.yes) {
24677
- const proceed = await p15.confirm({
24770
+ const proceed = await p16.confirm({
24678
24771
  message: [
24679
24772
  `Continue with ${pc5.bold(pc5.cyan("--force"))}?`,
24680
24773
  `${pc5.cyan("\u2502")} ${pc5.dim("This will force overwrite the existing admin code.")}`,
@@ -24682,8 +24775,8 @@ async function runInitCommand(name, options) {
24682
24775
  ].join("\n"),
24683
24776
  initialValue: true
24684
24777
  });
24685
- if (p15.isCancel(proceed) || !proceed) {
24686
- p15.cancel("Setup cancelled.");
24778
+ if (p16.isCancel(proceed) || !proceed) {
24779
+ p16.cancel("Setup cancelled.");
24687
24780
  process.exit(0);
24688
24781
  }
24689
24782
  forceMode = true;
@@ -24692,23 +24785,23 @@ async function runInitCommand(name, options) {
24692
24785
  await resolveForceInitNamespaces(cwd, namespace)
24693
24786
  );
24694
24787
  if (nuked > 0) {
24695
- p15.log.warn(`${pc5.yellow("Force mode:")} removed ${nuked} existing admin paths`);
24788
+ p16.log.warn(`${pc5.yellow("Force mode:")} removed ${nuked} existing admin paths`);
24696
24789
  }
24697
24790
  project2 = detectProject(cwd, namespace);
24698
24791
  }
24699
24792
  }
24700
24793
  } else {
24701
- p15.log.info("No Next.js app found \u2014 Running the fresh project mode...");
24794
+ p16.log.info("No Next.js app found \u2014 Running the fresh project mode...");
24702
24795
  let projectPrompt;
24703
24796
  try {
24704
24797
  projectPrompt = options.yes ? resolveNonInteractiveProject(name) : await promptProject(name);
24705
24798
  } catch (error) {
24706
- p15.log.error(error instanceof Error ? error.message : String(error));
24799
+ p16.log.error(error instanceof Error ? error.message : String(error));
24707
24800
  process.exit(1);
24708
24801
  }
24709
24802
  srcDir = projectPrompt.useSrcDir;
24710
24803
  if (!options.yes) {
24711
- const pmChoice = await p15.select({
24804
+ const pmChoice = await p16.select({
24712
24805
  message: "Which package manager do you want to use?",
24713
24806
  options: [
24714
24807
  { value: "pnpm", label: "pnpm", hint: "recommended" },
@@ -24717,8 +24810,8 @@ async function runInitCommand(name, options) {
24717
24810
  { value: "bun", label: "bun" }
24718
24811
  ]
24719
24812
  });
24720
- if (p15.isCancel(pmChoice)) {
24721
- p15.cancel("Setup cancelled.");
24813
+ if (p16.isCancel(pmChoice)) {
24814
+ p16.cancel("Setup cancelled.");
24722
24815
  process.exit(0);
24723
24816
  }
24724
24817
  pm = pmChoice;
@@ -24740,7 +24833,7 @@ async function runInitCommand(name, options) {
24740
24833
  ];
24741
24834
  if (srcDir) cnaArgs.push("--src-dir");
24742
24835
  else cnaArgs.push("--no-src-dir");
24743
- const createNextAppSpinner = p15.spinner();
24836
+ const createNextAppSpinner = p16.spinner();
24744
24837
  createNextAppSpinner.start(`Creating a Next.js app (latest)`);
24745
24838
  const createNextAppResult = await runQuietCommand(bin, cnaArgs, {
24746
24839
  cwd,
@@ -24752,8 +24845,8 @@ async function runInitCommand(name, options) {
24752
24845
  process.stderr.write(`${createNextAppResult.output.trimEnd()}
24753
24846
  `);
24754
24847
  }
24755
- p15.log.error(createNextAppResult.error);
24756
- p15.log.info(
24848
+ p16.log.error(createNextAppResult.error);
24849
+ p16.log.info(
24757
24850
  `You can create the project manually:
24758
24851
  ${pc5.cyan(`npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`)}
24759
24852
  Then run ${pc5.cyan("betterstart init")} inside it.`
@@ -24767,11 +24860,11 @@ async function runInitCommand(name, options) {
24767
24860
  );
24768
24861
  if (!hasPackageJson || !hasNextConfig) {
24769
24862
  createNextAppSpinner.stop(`Failed to create Next.js app: ${displayName}`);
24770
- p15.log.error(
24863
+ p16.log.error(
24771
24864
  "create-next-app completed but the project was not created. This can happen with nested npx calls."
24772
24865
  );
24773
24866
  const manualCmd = `npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`;
24774
- p15.log.info(
24867
+ p16.log.info(
24775
24868
  `Create the project manually:
24776
24869
  ${pc5.cyan(manualCmd)}
24777
24870
  Then run ${pc5.cyan("betterstart init")} inside it.`
@@ -24784,46 +24877,13 @@ async function runInitCommand(name, options) {
24784
24877
  project2 = detectProject(cwd, namespace);
24785
24878
  isFreshProject = true;
24786
24879
  }
24787
- const selectedPlugins = (() => {
24788
- if (options.plugins) {
24789
- return parsePluginList(options.plugins);
24790
- }
24791
- return [];
24792
- })();
24793
- const selectedIntegrations = (() => {
24794
- if (options.integrations) {
24795
- return parseIntegrationList(options.integrations);
24796
- }
24797
- return [];
24798
- })();
24799
- let collectedIntegrationConfig = {
24800
- sections: [],
24801
- overwriteKeys: /* @__PURE__ */ new Set()
24802
- };
24803
- let pluginSelection;
24804
- if (selectedPlugins.length > 0 || selectedIntegrations.length > 0) {
24805
- pluginSelection = {
24806
- plugins: selectedPlugins,
24807
- integrations: selectedIntegrations,
24808
- storage: selectedIntegrations.includes("r2") ? "r2" : "local"
24809
- };
24810
- } else if (options.yes) {
24811
- pluginSelection = { plugins: [], integrations: [], storage: "local" };
24812
- } else {
24813
- const promptResult = await promptPlugins(cwd);
24814
- collectedIntegrationConfig = promptResult.integrationConfig;
24815
- pluginSelection = {
24816
- plugins: promptResult.plugins,
24817
- integrations: promptResult.integrations,
24818
- storage: promptResult.storage
24819
- };
24820
- }
24821
24880
  let databaseUrl;
24881
+ let vercelResendApiKey;
24822
24882
  const existingDbUrl = readExistingDbUrl(cwd);
24823
24883
  if (options.yes) {
24824
24884
  if (options.databaseUrl) {
24825
24885
  if (!isValidDbUrl(options.databaseUrl)) {
24826
- p15.log.error(
24886
+ p16.log.error(
24827
24887
  `Invalid database URL. Must start with ${pc5.cyan("postgres://")} or ${pc5.cyan("postgresql://")}`
24828
24888
  );
24829
24889
  process.exit(1);
@@ -24842,34 +24902,77 @@ async function runInitCommand(name, options) {
24842
24902
  if (flow.ok && flow.databaseUrl) {
24843
24903
  databaseUrl = flow.databaseUrl;
24844
24904
  } else {
24845
- p15.log.warn("Vercel provisioning did not complete; continuing without a database URL.");
24905
+ p16.log.warn("Vercel provisioning did not complete; continuing without a database URL.");
24846
24906
  }
24847
24907
  }
24848
24908
  } else if (existingDbUrl) {
24849
24909
  const masked = maskDbUrl(existingDbUrl);
24850
- p15.log.info(`Using existing database URL from .env.local ${pc5.dim(`(${masked})`)}`);
24910
+ p16.log.info(`Using existing database URL from .env.local ${pc5.dim(`(${masked})`)}`);
24851
24911
  databaseUrl = existingDbUrl;
24852
24912
  } else {
24853
- const dbResult = await promptDatabase({ allowVercel: options.vercel !== false });
24854
- if (dbResult.provider === "vercel-cli") {
24913
+ const servicesResult = await promptServices({ allowVercel: options.vercel !== false });
24914
+ if (servicesResult.provider === "vercel-cli") {
24855
24915
  const flow = await runVercelNeonFlow({
24856
24916
  cwd,
24857
24917
  projectName,
24858
24918
  interactive: true,
24859
24919
  plan: options.vercelPlan,
24860
- env: process.env
24920
+ env: process.env,
24921
+ provisionResend: true
24861
24922
  });
24862
24923
  if (flow.ok && flow.databaseUrl) {
24863
24924
  databaseUrl = flow.databaseUrl;
24925
+ vercelResendApiKey = flow.resendApiKey;
24864
24926
  } else {
24865
- p15.log.info("Falling back to a manual database connection string.");
24927
+ p16.log.info("Falling back to a manual database connection string.");
24866
24928
  openBrowserVercelNeon();
24867
24929
  databaseUrl = await promptConnectionString();
24868
24930
  }
24869
24931
  } else {
24870
- databaseUrl = dbResult.url;
24932
+ databaseUrl = servicesResult.url;
24871
24933
  }
24872
24934
  }
24935
+ const selectedPlugins = (() => {
24936
+ if (options.plugins) {
24937
+ return parsePluginList(options.plugins);
24938
+ }
24939
+ return [];
24940
+ })();
24941
+ const selectedIntegrations = (() => {
24942
+ if (options.integrations) {
24943
+ return parseIntegrationList(options.integrations);
24944
+ }
24945
+ return [];
24946
+ })();
24947
+ let collectedIntegrationConfig = {
24948
+ sections: [],
24949
+ overwriteKeys: /* @__PURE__ */ new Set()
24950
+ };
24951
+ let pluginSelection;
24952
+ if (selectedPlugins.length > 0 || selectedIntegrations.length > 0) {
24953
+ pluginSelection = {
24954
+ plugins: selectedPlugins,
24955
+ integrations: selectedIntegrations,
24956
+ storage: selectedIntegrations.includes("r2") ? "r2" : "local"
24957
+ };
24958
+ if (vercelResendApiKey) {
24959
+ collectedIntegrationConfig.sections.push({
24960
+ header: "Email (Resend)",
24961
+ vars: [{ key: "BETTERSTART_RESEND_API_KEY", value: vercelResendApiKey }]
24962
+ });
24963
+ collectedIntegrationConfig.overwriteKeys.add("BETTERSTART_RESEND_API_KEY");
24964
+ }
24965
+ } else if (options.yes) {
24966
+ pluginSelection = { plugins: [], integrations: [], storage: "local" };
24967
+ } else {
24968
+ const promptResult = await promptPlugins(cwd, { vercelResendApiKey });
24969
+ collectedIntegrationConfig = promptResult.integrationConfig;
24970
+ pluginSelection = {
24971
+ plugins: promptResult.plugins,
24972
+ integrations: promptResult.integrations,
24973
+ storage: promptResult.storage
24974
+ };
24975
+ }
24873
24976
  const nextMajorVersion = detectNextMajorVersion(cwd);
24874
24977
  const config = {
24875
24978
  ...getDefaultConfig(srcDir, nextMajorVersion)
@@ -24879,7 +24982,7 @@ async function runInitCommand(name, options) {
24879
24982
  config.paths = config.frameworkConfig.next.paths;
24880
24983
  config.database.migrationsDir = deriveMigrationsDir(namespace);
24881
24984
  const results = [];
24882
- const s = p15.spinner();
24985
+ const s = p16.spinner();
24883
24986
  s.start("Directory structure");
24884
24987
  const baseFiles = scaffoldBase({
24885
24988
  cwd,
@@ -24956,7 +25059,7 @@ async function runInitCommand(name, options) {
24956
25059
  });
24957
25060
  s.stop("");
24958
25061
  process.stdout.write("\x1B[2A\x1B[J");
24959
- p15.note(noteLines.join("\n"), "Scaffolded admin");
25062
+ p16.note(noteLines.join("\n"), "Scaffolded admin");
24960
25063
  const drizzleConfigPath = path48.join(cwd, "drizzle.config.ts");
24961
25064
  if (!dbFiles.includes("drizzle.config.ts") && fs37.existsSync(drizzleConfigPath)) {
24962
25065
  if (forceMode) {
@@ -24966,20 +25069,20 @@ async function runInitCommand(name, options) {
24966
25069
  readNamespacedTemplate("drizzle.config.ts", namespace),
24967
25070
  "utf-8"
24968
25071
  );
24969
- p15.log.success("Updated drizzle.config.ts");
25072
+ p16.log.success("Updated drizzle.config.ts");
24970
25073
  } else if (!options.yes) {
24971
- const overwrite = await p15.confirm({
25074
+ const overwrite = await p16.confirm({
24972
25075
  message: "drizzle.config.ts already exists. Overwrite with latest version?",
24973
25076
  initialValue: true
24974
25077
  });
24975
- if (!p15.isCancel(overwrite) && overwrite) {
25078
+ if (!p16.isCancel(overwrite) && overwrite) {
24976
25079
  const { readNamespacedTemplate } = await import("./template-reader-X6KFP7B3.js");
24977
25080
  fs37.writeFileSync(
24978
25081
  drizzleConfigPath,
24979
25082
  readNamespacedTemplate("drizzle.config.ts", namespace),
24980
25083
  "utf-8"
24981
25084
  );
24982
- p15.log.success("Updated drizzle.config.ts");
25085
+ p16.log.success("Updated drizzle.config.ts");
24983
25086
  }
24984
25087
  }
24985
25088
  }
@@ -25005,8 +25108,8 @@ async function runInitCommand(name, options) {
25005
25108
  depsInstalled = true;
25006
25109
  } else {
25007
25110
  s.stop("Failed to install dependencies");
25008
- p15.log.warning(depsResult.error ?? "Unknown error");
25009
- p15.log.info(
25111
+ p16.log.warning(depsResult.error ?? "Unknown error");
25112
+ p16.log.info(
25010
25113
  `You can install them manually:
25011
25114
  ${pc5.cyan(`${pm} add ${depsResult.dependencies.join(" ")}`)}
25012
25115
  ${pc5.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
@@ -25095,10 +25198,10 @@ async function runInitCommand(name, options) {
25095
25198
  ` ${pc5.yellow("\u25B2")} Local filesystem storage is only for development or self-hosted persistent-disk deployments. Use Cloudflare R2 or AWS S3 for hosted/serverless production.`
25096
25199
  );
25097
25200
  }
25098
- p15.note(installLines.join("\n"), "Installed");
25201
+ p16.note(installLines.join("\n"), "Installed");
25099
25202
  let dbPushed = false;
25100
25203
  if (depsResult.success && options.skipMigration && hasDbUrl(cwd)) {
25101
- p15.log.info(`Skipping database schema push ${pc5.dim("(--skip-migration)")}`);
25204
+ p16.log.info(`Skipping database schema push ${pc5.dim("(--skip-migration)")}`);
25102
25205
  } else if (depsResult.success && hasDbUrl(cwd)) {
25103
25206
  let driverReady = hasDrizzleKitPostgresDriverDependency(cwd);
25104
25207
  if (!driverReady) {
@@ -25113,8 +25216,8 @@ async function runInitCommand(name, options) {
25113
25216
  });
25114
25217
  if (!driverResult.success) {
25115
25218
  s.stop("Database push failed");
25116
- p15.log.warning(driverResult.error ?? `Failed to install ${DRIZZLE_KIT_POSTGRES_DRIVER_DEP}`);
25117
- p15.log.info(
25219
+ p16.log.warning(driverResult.error ?? `Failed to install ${DRIZZLE_KIT_POSTGRES_DRIVER_DEP}`);
25220
+ p16.log.info(
25118
25221
  `Install ${DRIZZLE_KIT_POSTGRES_DRIVER_DEP} manually, then run: ${pc5.cyan(drizzlePushCommand(pm))}`
25119
25222
  );
25120
25223
  } else {
@@ -25125,7 +25228,7 @@ async function runInitCommand(name, options) {
25125
25228
  if (driverReady) {
25126
25229
  const useTerminalForDbPush = shouldUseTerminalForDbPush(options);
25127
25230
  if (useTerminalForDbPush) {
25128
- p15.log.info(`Running ${pc5.cyan("drizzle-kit push --force")}`);
25231
+ p16.log.info(`Running ${pc5.cyan("drizzle-kit push --force")}`);
25129
25232
  } else {
25130
25233
  s.start("Pushing database schema (drizzle-kit push)");
25131
25234
  }
@@ -25134,13 +25237,13 @@ async function runInitCommand(name, options) {
25134
25237
  if (useTerminalForDbPush) {
25135
25238
  const verification = await verifyDatabaseReachable(cwd);
25136
25239
  if (!verification.success) {
25137
- p15.log.warning(verification.error);
25138
- p15.log.error("Database was not reachable. Aborting setup.");
25240
+ p16.log.warning(verification.error);
25241
+ p16.log.error("Database was not reachable. Aborting setup.");
25139
25242
  process.exit(1);
25140
25243
  }
25141
25244
  }
25142
25245
  if (useTerminalForDbPush) {
25143
- p15.log.success("Database schema pushed");
25246
+ p16.log.success("Database schema pushed");
25144
25247
  } else {
25145
25248
  s.stop(`${pc5.green("\u2713")} Database schema pushed`);
25146
25249
  }
@@ -25150,12 +25253,12 @@ async function runInitCommand(name, options) {
25150
25253
  s.stop("Database push failed");
25151
25254
  }
25152
25255
  const pushError = pushResult.error ?? "Unknown error";
25153
- p15.log.warning(pushError);
25256
+ p16.log.warning(pushError);
25154
25257
  if (isDatabaseReachabilityError(pushError)) {
25155
- p15.log.error("Database was not reachable. Aborting setup.");
25258
+ p16.log.error("Database was not reachable. Aborting setup.");
25156
25259
  process.exit(1);
25157
25260
  }
25158
- p15.log.info(`You can run it manually: ${pc5.cyan(drizzlePushCommand(pm))}`);
25261
+ p16.log.info(`You can run it manually: ${pc5.cyan(drizzlePushCommand(pm))}`);
25159
25262
  }
25160
25263
  }
25161
25264
  }
@@ -25164,7 +25267,7 @@ async function runInitCommand(name, options) {
25164
25267
  let seedSuccess = false;
25165
25268
  let adminAccountReady = false;
25166
25269
  if (dbPushed && options.skipAdminCreation) {
25167
- p15.log.info(
25270
+ p16.log.info(
25168
25271
  `Skipping admin user creation ${pc5.dim(`(use ${betterstartExecCommand(pm, "seed")} later)`)}`
25169
25272
  );
25170
25273
  }
@@ -25174,14 +25277,14 @@ async function runInitCommand(name, options) {
25174
25277
  let replaceExistingAdmin = false;
25175
25278
  const adminCheck = await checkExistingAdmin(cwd, adminDir, authBasePath);
25176
25279
  if (adminCheck.error) {
25177
- p15.log.warning(`Could not verify existing admin account ${pc5.dim(`(${adminCheck.error})`)}`);
25280
+ p16.log.warning(`Could not verify existing admin account ${pc5.dim(`(${adminCheck.error})`)}`);
25178
25281
  if (isDatabaseReachabilityError(adminCheck.error)) {
25179
- p15.log.error("Database was not reachable. Aborting setup.");
25282
+ p16.log.error("Database was not reachable. Aborting setup.");
25180
25283
  process.exit(1);
25181
25284
  }
25182
25285
  } else if (adminCheck.existingAdmin) {
25183
25286
  const existingAdminLabel = formatAdminIdentity(adminCheck.existingAdmin);
25184
- const adminAction = await p15.select({
25287
+ const adminAction = await p16.select({
25185
25288
  message: "Found an already existing admin account. Do you want to replace it or skip?",
25186
25289
  options: [
25187
25290
  {
@@ -25195,34 +25298,34 @@ async function runInitCommand(name, options) {
25195
25298
  }
25196
25299
  ]
25197
25300
  });
25198
- if (p15.isCancel(adminAction)) {
25199
- p15.cancel("Setup cancelled.");
25301
+ if (p16.isCancel(adminAction)) {
25302
+ p16.cancel("Setup cancelled.");
25200
25303
  process.exit(0);
25201
25304
  }
25202
25305
  if (adminAction === "skip") {
25203
25306
  adminAccountReady = true;
25204
- p15.log.info(`Keeping existing admin account ${pc5.dim(`(${existingAdminLabel})`)}`);
25307
+ p16.log.info(`Keeping existing admin account ${pc5.dim(`(${existingAdminLabel})`)}`);
25205
25308
  } else {
25206
25309
  replaceExistingAdmin = true;
25207
25310
  }
25208
25311
  }
25209
25312
  if (!adminAccountReady) {
25210
- p15.note(
25313
+ p16.note(
25211
25314
  pc5.dim(
25212
25315
  replaceExistingAdmin ? "Replace the existing admin account to access the admin." : "Create your first admin user to access the admin."
25213
25316
  ),
25214
25317
  "Admin account"
25215
25318
  );
25216
- const credentials = await p15.group(
25319
+ const credentials = await p16.group(
25217
25320
  {
25218
- email: () => p15.text({
25321
+ email: () => p16.text({
25219
25322
  message: "Admin email",
25220
25323
  placeholder: "admin@example.com",
25221
25324
  validate: (v) => {
25222
25325
  if (!v || !v.includes("@")) return "Please enter a valid email";
25223
25326
  }
25224
25327
  }),
25225
- password: () => p15.password({
25328
+ password: () => p16.password({
25226
25329
  message: "Admin password",
25227
25330
  validate: (v) => {
25228
25331
  if (!v || v.length < 8) return "Password must be at least 8 characters";
@@ -25231,7 +25334,7 @@ async function runInitCommand(name, options) {
25231
25334
  },
25232
25335
  {
25233
25336
  onCancel: () => {
25234
- p15.cancel("Setup cancelled.");
25337
+ p16.cancel("Setup cancelled.");
25235
25338
  process.exit(0);
25236
25339
  }
25237
25340
  }
@@ -25250,11 +25353,11 @@ async function runInitCommand(name, options) {
25250
25353
  );
25251
25354
  if (seedResult.existingUser) {
25252
25355
  s.stop(`${pc5.yellow("\u25B2")} An account already exists for ${credentials.email}`);
25253
- const replace = await p15.confirm({
25356
+ const replace = await p16.confirm({
25254
25357
  message: "Replace the existing account with this email?",
25255
25358
  initialValue: false
25256
25359
  });
25257
- if (!p15.isCancel(replace) && replace) {
25360
+ if (!p16.isCancel(replace) && replace) {
25258
25361
  seedOverwriteMode = "email";
25259
25362
  s.start("Replacing admin user");
25260
25363
  seedResult = await runSeed(
@@ -25275,14 +25378,14 @@ async function runInitCommand(name, options) {
25275
25378
  adminAccountReady = true;
25276
25379
  } else if (seedResult.error) {
25277
25380
  s.stop(`${pc5.red("\u2717")} Failed to create admin user`);
25278
- p15.note(
25381
+ p16.note(
25279
25382
  `${pc5.red(seedResult.error)}
25280
25383
 
25281
25384
  Run manually: ${pc5.cyan(betterstartExecCommand(pm, "seed"))}`,
25282
25385
  pc5.red("Seed failed")
25283
25386
  );
25284
25387
  if (isDatabaseReachabilityError(seedResult.error)) {
25285
- p15.log.error("Database was not reachable. Aborting setup.");
25388
+ p16.log.error("Database was not reachable. Aborting setup.");
25286
25389
  process.exit(1);
25287
25390
  }
25288
25391
  }
@@ -25336,15 +25439,15 @@ Run manually: ${pc5.cyan(betterstartExecCommand(pm, "seed"))}`,
25336
25439
  ` ${step++}. Run ${pc5.cyan(betterstartExecCommand(pm, "add --integration <name>"))} to install integrations later`
25337
25440
  );
25338
25441
  summaryLines.push("", "Next steps:", ...nextSteps);
25339
- p15.note(summaryLines.join("\n"), "Admin scaffolded successfully");
25442
+ p16.note(summaryLines.join("\n"), "Admin scaffolded successfully");
25340
25443
  if (!options.yes && !options.skipDevServerStart) {
25341
25444
  const devCmd = runCommand(pm, "dev");
25342
- const startDev = await p15.confirm({
25445
+ const startDev = await p16.confirm({
25343
25446
  message: "Start the development server?",
25344
25447
  initialValue: true
25345
25448
  });
25346
- if (!p15.isCancel(startDev) && startDev) {
25347
- p15.outro(`Starting ${pc5.cyan(devCmd)}...`);
25449
+ if (!p16.isCancel(startDev) && startDev) {
25450
+ p16.outro(`Starting ${pc5.cyan(devCmd)}...`);
25348
25451
  await startManagedDevServer(cwd, devCmd, adminLoginUrl, {
25349
25452
  email: seedSuccess && seedEmail ? seedEmail : void 0,
25350
25453
  password: seedSuccess && seedPassword ? seedPassword : void 0
@@ -25352,7 +25455,7 @@ Run manually: ${pc5.cyan(betterstartExecCommand(pm, "seed"))}`,
25352
25455
  return;
25353
25456
  }
25354
25457
  }
25355
- p15.outro("Done!");
25458
+ p16.outro("Done!");
25356
25459
  }
25357
25460
  function isValidDbUrl(url) {
25358
25461
  return url.startsWith("postgres://") || url.startsWith("postgresql://");
@@ -25739,7 +25842,7 @@ function printAdminReadyNote(state) {
25739
25842
  lines.unshift(`Password: ${pc5.cyan(state.adminPassword)}`);
25740
25843
  lines.unshift(`Admin user: ${pc5.cyan(state.adminEmail)}`);
25741
25844
  }
25742
- p15.note(lines.join("\n"), "Admin ready");
25845
+ p16.note(lines.join("\n"), "Admin ready");
25743
25846
  }
25744
25847
  function shouldSuppressDevServerStartupLine(line) {
25745
25848
  const plain = stripAnsi(line).trim();
@@ -25851,7 +25954,7 @@ function startManagedDevServer(cwd, devCmd, adminLoginUrl, adminCredentials) {
25851
25954
 
25852
25955
  // adapters/next/commands/list-integrations.ts
25853
25956
  import path49 from "path";
25854
- import * as p16 from "@clack/prompts";
25957
+ import * as p17 from "@clack/prompts";
25855
25958
  async function runListIntegrationsCommand(options) {
25856
25959
  const cwd = options.cwd ? path49.resolve(options.cwd) : process.cwd();
25857
25960
  const config = await resolveConfig(cwd);
@@ -25860,15 +25963,15 @@ async function runListIntegrationsCommand(options) {
25860
25963
  const status = installedIntegrations.has(integration.id) ? "installed" : "available";
25861
25964
  return `${integration.id.padEnd(10)} ${status.padEnd(10)} ${integration.kind.padEnd(8)} ${integration.description}`;
25862
25965
  });
25863
- p16.note(lines.join("\n"), "BetterStart integrations");
25864
- p16.outro(
25966
+ p17.note(lines.join("\n"), "BetterStart integrations");
25967
+ p17.outro(
25865
25968
  `${installedIntegrations.size} installed, ${lines.length - installedIntegrations.size} available`
25866
25969
  );
25867
25970
  }
25868
25971
 
25869
25972
  // adapters/next/commands/list-plugins.ts
25870
25973
  import path50 from "path";
25871
- import * as p17 from "@clack/prompts";
25974
+ import * as p18 from "@clack/prompts";
25872
25975
  async function runListPluginsCommand(options) {
25873
25976
  const cwd = options.cwd ? path50.resolve(options.cwd) : process.cwd();
25874
25977
  const config = await resolveConfig(cwd);
@@ -25877,34 +25980,34 @@ async function runListPluginsCommand(options) {
25877
25980
  const status = installedPlugins.has(plugin.id) ? "installed" : "available";
25878
25981
  return `${plugin.id.padEnd(10)} ${status.padEnd(10)} ${plugin.kind.padEnd(12)} ${plugin.description}`;
25879
25982
  });
25880
- p17.note(lines.join("\n"), "BetterStart plugins");
25881
- p17.outro(`${installedPlugins.size} installed, ${lines.length - installedPlugins.size} available`);
25983
+ p18.note(lines.join("\n"), "BetterStart plugins");
25984
+ p18.outro(`${installedPlugins.size} installed, ${lines.length - installedPlugins.size} available`);
25882
25985
  }
25883
25986
 
25884
25987
  // adapters/next/commands/remove.ts
25885
25988
  import path51 from "path";
25886
- import * as p18 from "@clack/prompts";
25989
+ import * as p19 from "@clack/prompts";
25887
25990
  async function runRemoveCommand(items, options) {
25888
25991
  const removeIntegrationsMode = Boolean(options.integration);
25889
25992
  if (!removeIntegrationsMode && items.includes("core")) {
25890
- p18.log.error("The core Admin cannot be removed.");
25993
+ p19.log.error("The core Admin cannot be removed.");
25891
25994
  process.exit(1);
25892
25995
  }
25893
25996
  const pluginIds = items.filter(isPluginId);
25894
25997
  const integrationIds = items.filter(isIntegrationId);
25895
25998
  if (!removeIntegrationsMode && integrationIds.length > 0) {
25896
- p18.log.error(
25999
+ p19.log.error(
25897
26000
  `Integration IDs require --integration. Run \`betterstart remove --integration ${integrationIds.join(" ")}\`.`
25898
26001
  );
25899
26002
  process.exit(1);
25900
26003
  }
25901
26004
  if (removeIntegrationsMode && pluginIds.length > 0) {
25902
- p18.log.error(`Plugin IDs cannot be removed with --integration: ${pluginIds.join(", ")}`);
26005
+ p19.log.error(`Plugin IDs cannot be removed with --integration: ${pluginIds.join(", ")}`);
25903
26006
  process.exit(1);
25904
26007
  }
25905
26008
  const invalidItems = removeIntegrationsMode ? items.filter((integrationId) => !isIntegrationId(integrationId)) : items.filter((pluginId) => !isPluginId(pluginId));
25906
26009
  if (invalidItems.length > 0) {
25907
- p18.log.error(
26010
+ p19.log.error(
25908
26011
  removeIntegrationsMode ? formatUnknownIntegrationMessage(invalidItems) : formatUnknownPluginMessage(invalidItems)
25909
26012
  );
25910
26013
  process.exit(1);
@@ -25913,12 +26016,12 @@ async function runRemoveCommand(items, options) {
25913
26016
  const config = await resolveConfig(cwd);
25914
26017
  const pm = detectPackageManager(cwd);
25915
26018
  if (!options.force) {
25916
- const confirmed = await p18.confirm({
26019
+ const confirmed = await p19.confirm({
25917
26020
  message: `Remove ${removeIntegrationsMode ? "integration" : "plugin"}${items.length === 1 ? "" : "s"} ${items.join(", ")}?`,
25918
26021
  initialValue: false
25919
26022
  });
25920
- if (p18.isCancel(confirmed) || !confirmed) {
25921
- p18.cancel(`${removeIntegrationsMode ? "Integration" : "Plugin"} removal cancelled.`);
26023
+ if (p19.isCancel(confirmed) || !confirmed) {
26024
+ p19.cancel(`${removeIntegrationsMode ? "Integration" : "Plugin"} removal cancelled.`);
25922
26025
  process.exit(0);
25923
26026
  }
25924
26027
  }
@@ -25931,13 +26034,13 @@ async function runRemoveCommand(items, options) {
25931
26034
  });
25932
26035
  writeConfigFile(cwd, result2.config);
25933
26036
  if (result2.removed.length === 0) {
25934
- p18.outro("No integrations were removed.");
26037
+ p19.outro("No integrations were removed.");
25935
26038
  return;
25936
26039
  }
25937
26040
  if (result2.warnings.length > 0) {
25938
- p18.note(result2.warnings.join("\n"), "Warnings");
26041
+ p19.note(result2.warnings.join("\n"), "Warnings");
25939
26042
  }
25940
- p18.outro(
26043
+ p19.outro(
25941
26044
  `Removed integration${result2.removed.length === 1 ? "" : "s"}: ${result2.removed.join(", ")}`
25942
26045
  );
25943
26046
  return;
@@ -25950,13 +26053,13 @@ async function runRemoveCommand(items, options) {
25950
26053
  });
25951
26054
  writeConfigFile(cwd, result.config);
25952
26055
  if (result.removed.length === 0) {
25953
- p18.outro("No plugins were removed.");
26056
+ p19.outro("No plugins were removed.");
25954
26057
  return;
25955
26058
  }
25956
26059
  if (result.warnings.length > 0) {
25957
- p18.note(result.warnings.join("\n"), "Warnings");
26060
+ p19.note(result.warnings.join("\n"), "Warnings");
25958
26061
  }
25959
- p18.outro(`Removed plugin${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`);
26062
+ p19.outro(`Removed plugin${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`);
25960
26063
  }
25961
26064
 
25962
26065
  // adapters/next/commands/remove-schema.ts
@@ -26105,7 +26208,7 @@ async function runRemoveSchemaCommand(schemaName, options) {
26105
26208
  // adapters/next/commands/uninstall.ts
26106
26209
  import fs40 from "fs";
26107
26210
  import path53 from "path";
26108
- import * as p19 from "@clack/prompts";
26211
+ import * as p20 from "@clack/prompts";
26109
26212
  import pc6 from "picocolors";
26110
26213
 
26111
26214
  // adapters/next/commands/uninstall-cleaners.ts
@@ -26327,8 +26430,8 @@ function buildUninstallPlan(cwd, namespaceValue) {
26327
26430
  count: configFiles.length,
26328
26431
  unit: configFiles.length === 1 ? "file" : "files",
26329
26432
  execute() {
26330
- for (const p20 of configPaths) {
26331
- if (fs40.existsSync(p20)) fs40.unlinkSync(p20);
26433
+ for (const p21 of configPaths) {
26434
+ if (fs40.existsSync(p21)) fs40.unlinkSync(p21);
26332
26435
  }
26333
26436
  }
26334
26437
  });
@@ -26392,7 +26495,7 @@ function buildUninstallPlan(cwd, namespaceValue) {
26392
26495
  }
26393
26496
  async function runUninstallCommand(options) {
26394
26497
  const cwd = options.cwd ? path53.resolve(options.cwd) : process.cwd();
26395
- p19.intro(pc6.bgRed(pc6.white(" BetterStart Uninstall ")));
26498
+ p20.intro(pc6.bgRed(pc6.white(" BetterStart Uninstall ")));
26396
26499
  let namespace = DEFAULT_ADMIN_NAMESPACE;
26397
26500
  try {
26398
26501
  const config = await resolveConfig(cwd);
@@ -26401,8 +26504,8 @@ async function runUninstallCommand(options) {
26401
26504
  }
26402
26505
  const steps = buildUninstallPlan(cwd, namespace);
26403
26506
  if (steps.length === 0) {
26404
- p19.log.success(`${pc6.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
26405
- p19.outro("Done");
26507
+ p20.log.success(`${pc6.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
26508
+ p20.outro("Done");
26406
26509
  return;
26407
26510
  }
26408
26511
  const planLines = steps.map((step) => {
@@ -26410,18 +26513,18 @@ async function runUninstallCommand(options) {
26410
26513
  const countLabel = pc6.dim(`${step.count} ${step.unit}`);
26411
26514
  return `${pc6.red("\xD7")} ${names} ${countLabel}`;
26412
26515
  });
26413
- p19.note(planLines.join("\n"), "Uninstall plan");
26516
+ p20.note(planLines.join("\n"), "Uninstall plan");
26414
26517
  if (!options.force) {
26415
- const confirmed = await p19.confirm({
26518
+ const confirmed = await p20.confirm({
26416
26519
  message: "Proceed with uninstall?",
26417
26520
  initialValue: false
26418
26521
  });
26419
- if (p19.isCancel(confirmed) || !confirmed) {
26420
- p19.cancel("Uninstall cancelled.");
26522
+ if (p20.isCancel(confirmed) || !confirmed) {
26523
+ p20.cancel("Uninstall cancelled.");
26421
26524
  process.exit(0);
26422
26525
  }
26423
26526
  }
26424
- const s = p19.spinner();
26527
+ const s = p20.spinner();
26425
26528
  s.start(steps[0].label);
26426
26529
  for (const step of steps) {
26427
26530
  s.message(step.label);
@@ -26429,8 +26532,8 @@ async function runUninstallCommand(options) {
26429
26532
  }
26430
26533
  const parts = steps.map((step) => `${step.count} ${step.unit}`);
26431
26534
  s.stop(`Removed ${parts.join(", ")}`);
26432
- p19.note(pc6.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
26433
- p19.outro("Uninstall complete");
26535
+ p20.note(pc6.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
26536
+ p20.outro("Uninstall complete");
26434
26537
  }
26435
26538
 
26436
26539
  // adapters/next/commands/update-component.ts