betterstart-cli 0.0.40 → 0.0.42

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
@@ -1508,8 +1508,15 @@ function runDrizzlePush(cwd, options = {}) {
1508
1508
  });
1509
1509
  let stdout = "";
1510
1510
  let stderr = "";
1511
+ let outputNotified = false;
1512
+ const notifyOutput = () => {
1513
+ if (outputNotified) return;
1514
+ outputNotified = true;
1515
+ options.onOutput?.();
1516
+ };
1511
1517
  const stderrFilter = createDrizzleNoiseFilter((chunk) => {
1512
1518
  if (options.interactive) {
1519
+ notifyOutput();
1513
1520
  process.stderr.write(chunk);
1514
1521
  return;
1515
1522
  }
@@ -1517,6 +1524,7 @@ function runDrizzlePush(cwd, options = {}) {
1517
1524
  });
1518
1525
  const stdoutFilter = createDrizzleNoiseFilter((chunk) => {
1519
1526
  if (options.interactive) {
1527
+ notifyOutput();
1520
1528
  process.stdout.write(chunk);
1521
1529
  return;
1522
1530
  }
@@ -22241,7 +22249,7 @@ async function promptPlugins(cwd, options = {}) {
22241
22249
  } else {
22242
22250
  if (existingToken) {
22243
22251
  p12.log.info(
22244
- `Using the existing Vercel Blob token from .env.local ${styleText(
22252
+ `Using the existing BLOB_READ_WRITE_TOKEN from .env.local ${styleText(
22245
22253
  "dim",
22246
22254
  `(${maskBlobToken(existingToken)})`
22247
22255
  )}`
@@ -22270,30 +22278,13 @@ async function promptPlugins(cwd, options = {}) {
22270
22278
  process.exit(0);
22271
22279
  }
22272
22280
  const integrations = [];
22273
- const setupResend = await p12.confirm({
22274
- message: `Do you want to setup resend.com emails?
22275
- ${styleText(
22276
- "dim",
22277
- "This will be used for form submission confirmations, password resets etc."
22278
- )}`,
22279
- initialValue: true
22281
+ sections.push({
22282
+ header: "Email (Resend)",
22283
+ vars: [
22284
+ { key: "BETTERSTART_RESEND_API_KEY", value: "" },
22285
+ { key: "BETTERSTART_EMAIL_FROM", value: "" }
22286
+ ]
22280
22287
  });
22281
- if (p12.isCancel(setupResend)) {
22282
- p12.cancel("Setup cancelled.");
22283
- process.exit(0);
22284
- }
22285
- if (setupResend) {
22286
- integrations.push("resend");
22287
- mergeIntegrationConfig(await collectIntegrationConfig(cwd, ["resend"]));
22288
- } else {
22289
- sections.push({
22290
- header: "Email (Resend)",
22291
- vars: [
22292
- { key: "BETTERSTART_RESEND_API_KEY", value: "" },
22293
- { key: "BETTERSTART_EMAIL_FROM", value: "" }
22294
- ]
22295
- });
22296
- }
22297
22288
  if (storage === "r2") {
22298
22289
  integrations.push("r2");
22299
22290
  } else if (storage === "vercel-blob") {
@@ -25505,18 +25496,6 @@ function readNamespaceFromConfigSource(source) {
25505
25496
  const match = source.match(/\bnamespace\s*:\s*(['"`])([^'"`]+)\1/);
25506
25497
  return normalizeNamespaceForRemoval(match?.[2]);
25507
25498
  }
25508
- function formatStorageProviderLabel(provider) {
25509
- switch (provider) {
25510
- case "vercel-blob":
25511
- return "Vercel Blob";
25512
- case "r2":
25513
- return "Cloudflare R2";
25514
- case "local":
25515
- return "local filesystem";
25516
- default:
25517
- return provider;
25518
- }
25519
- }
25520
25499
  function resolveNonInteractiveProject(name) {
25521
25500
  const projectName = name?.trim() || "my-app";
25522
25501
  if (projectName !== "." && !/^[a-z0-9_-]+$/i.test(projectName)) {
@@ -25808,7 +25787,7 @@ async function runInitCommand(name, options) {
25808
25787
  }
25809
25788
  } else if (existingDbUrl) {
25810
25789
  const masked = maskDbUrl(existingDbUrl);
25811
- p18.log.info(`Using existing database URL from .env.local ${pc7.dim(`(${masked})`)}`);
25790
+ p18.log.info(`Using the existing DATABASE_URL from .env.local ${pc7.dim(`(${masked})`)}`);
25812
25791
  databaseUrl = existingDbUrl;
25813
25792
  } else {
25814
25793
  const databaseStep = deferNextPromptCheck();
@@ -25913,35 +25892,21 @@ async function runInitCommand(name, options) {
25913
25892
  config.frameworkConfig.next.paths = deriveNamespacePaths(namespace, srcDir);
25914
25893
  config.paths = config.frameworkConfig.next.paths;
25915
25894
  config.database.migrationsDir = deriveMigrationsDir(namespace);
25916
- const results = [];
25917
25895
  const s = spinner2();
25918
25896
  s.start("Directory structure");
25919
- const baseFiles = scaffoldBase({
25897
+ scaffoldBase({
25920
25898
  cwd,
25921
25899
  config,
25922
25900
  includeEmailTemplatesDir: pluginSelection.integrations.some(isEmailProviderIntegration)
25923
25901
  });
25924
- results.push({ label: "Directory structure", result: `${baseFiles.length} files` });
25925
25902
  s.message("TypeScript aliases");
25926
- const tsResult = scaffoldTsconfig(cwd, config);
25927
- results.push({
25928
- label: "TypeScript aliases",
25929
- result: tsResult.added.length > 0 ? `${tsResult.added.length} paths` : "already set"
25930
- });
25903
+ scaffoldTsconfig(cwd, config);
25931
25904
  s.message("Tailwind CSS");
25932
- const twResult = scaffoldTailwind(cwd, srcDir, namespace);
25933
- results.push({
25934
- label: "Tailwind CSS",
25935
- result: twResult.appended ? "updated" : twResult.file ? "already set" : "no CSS file"
25936
- });
25905
+ scaffoldTailwind(cwd, srcDir, namespace);
25937
25906
  s.message("Dev server");
25938
- const devServerResult = ensureDefaultDevScriptPort(cwd);
25939
- results.push({
25940
- label: "Dev server",
25941
- result: formatDevServerResult(devServerResult)
25942
- });
25907
+ ensureDefaultDevScriptPort(cwd);
25943
25908
  s.message("Environment variables");
25944
- const envResult = scaffoldEnv(cwd, { databaseUrl, namespace });
25909
+ scaffoldEnv(cwd, { databaseUrl, namespace });
25945
25910
  if (collectedIntegrationConfig.sections.length > 0) {
25946
25911
  appendEnvVars(
25947
25912
  cwd,
@@ -25949,49 +25914,26 @@ async function runInitCommand(name, options) {
25949
25914
  collectedIntegrationConfig.overwriteKeys
25950
25915
  );
25951
25916
  }
25952
- const envCount = envResult.added.length + envResult.updated.length;
25953
- results.push({
25954
- label: "Environment variables",
25955
- result: envCount > 0 ? `${envCount} vars` : "already set"
25956
- });
25957
25917
  s.message("Database");
25958
25918
  const dbFiles = scaffoldDatabase({ cwd, config });
25959
- results.push({ label: "Database", result: `${dbFiles.length} files` });
25960
25919
  s.message("Authentication");
25961
- const authFiles = scaffoldAuth({ cwd, config });
25962
- results.push({ label: "Authentication", result: `${authFiles.length} files` });
25920
+ scaffoldAuth({ cwd, config });
25963
25921
  s.message("Components");
25964
- const compFiles = scaffoldComponents({ cwd, config });
25965
- results.push({ label: "Components", result: `${compFiles.length} files` });
25922
+ scaffoldComponents({ cwd, config });
25966
25923
  s.message("Pages & layouts");
25967
- const layoutFiles = scaffoldLayout({ cwd, config });
25968
- results.push({ label: "Pages & layouts", result: `${layoutFiles.length} files` });
25924
+ scaffoldLayout({ cwd, config });
25969
25925
  s.message("API routes");
25970
- const apiFiles = scaffoldApiRoutes({ cwd, config });
25971
- results.push({ label: "API routes", result: `${apiFiles.length} routes` });
25926
+ scaffoldApiRoutes({ cwd, config });
25972
25927
  s.message("Linter");
25973
- let linterResult;
25974
25928
  if (project2.linter.type === "none") {
25975
- const biomeResult = scaffoldBiome(cwd, project2.linter);
25976
- linterResult = biomeResult.installed ? "biome (new)" : "none";
25977
- } else {
25978
- linterResult = project2.linter.type;
25929
+ scaffoldBiome(cwd, project2.linter);
25979
25930
  }
25980
- results.push({ label: "Linter", result: linterResult });
25981
25931
  s.message("Next.js config");
25982
25932
  const nextConfigResult = scaffoldNextConfig({ cwd, nextMajorVersion });
25983
- results.push({
25984
- label: "Next.js config",
25985
- result: nextConfigResult.status === "created" ? "created" : nextConfigResult.status === "updated" ? "updated" : nextConfigResult.status === "already-set" ? "already set" : nextConfigResult.status === "not-needed" ? "not needed" : "manual setup needed"
25986
- });
25987
- const maxLabel = Math.max(...results.map((r) => r.label.length));
25988
- const noteLines = results.map((r) => {
25989
- const padded = r.label.padEnd(maxLabel + 3);
25990
- return `${pc7.green("\u2713")} ${padded}${pc7.dim(r.result)}`;
25991
- });
25992
- s.stop("");
25993
- process.stdout.write("\x1B[2A\x1B[J");
25994
- p18.note(noteLines.join("\n"), "Scaffolded admin");
25933
+ s.clear();
25934
+ if (nextConfigResult.status === "unsupported") {
25935
+ p18.log.warn("The Next.js config could not be updated automatically \u2014 review it manually.");
25936
+ }
25995
25937
  const drizzleConfigPath = path50.join(cwd, "drizzle.config.ts");
25996
25938
  if (!dbFiles.includes("drizzle.config.ts") && fs39.existsSync(drizzleConfigPath)) {
25997
25939
  if (forceMode) {
@@ -26096,42 +26038,20 @@ async function runInitCommand(name, options) {
26096
26038
  }
26097
26039
  writeConfigFile(cwd, resolvedIntegrationInstallResult.config);
26098
26040
  const usesLocalStorage = resolvedIntegrationInstallResult.config.storage.provider === "local";
26099
- const installLines = [];
26100
- if (depsInstalled) {
26101
- installLines.push(
26102
- `${pc7.green("\u2713")} Dependencies ${pc7.dim(`${depsResult.dependencies.length} deps + ${depsResult.devDeps.length} dev deps`)}`
26103
- );
26104
- }
26105
- if (coreSchemasResult.errors.length > 0) {
26106
- installLines.push(
26107
- `${pc7.yellow("\u25B2")} Core schemas ${pc7.dim(`${coreSchemasResult.errors.length} warning(s)`)}`
26108
- );
26109
- for (const err of coreSchemasResult.errors) {
26110
- installLines.push(` ${pc7.dim(err)}`);
26111
- }
26112
- } else {
26113
- installLines.push(
26114
- `${pc7.green("\u2713")} Core schemas ${pc7.dim(`${coreSchemasResult.schemas.length} schemas, ${coreSchemasResult.generatedFiles.length} files`)}`
26115
- );
26041
+ for (const err of coreSchemasResult.errors) {
26042
+ p18.log.warn(`Core schemas: ${err}`);
26116
26043
  }
26117
- installLines.push(
26118
- `${pc7.green("\u2713")} Plugins ${pc7.dim(resolvedPluginInstallResult.installed.length > 0 ? resolvedPluginInstallResult.installed.join(", ") : "core only")}`
26119
- );
26120
26044
  for (const warning of resolvedPluginInstallResult.warnings) {
26121
- installLines.push(` ${pc7.yellow("\u25B2")} ${warning}`);
26045
+ p18.log.warn(warning);
26122
26046
  }
26123
- installLines.push(
26124
- `${pc7.green("\u2713")} Integrations ${pc7.dim(resolvedIntegrationInstallResult.installed.length > 0 ? resolvedIntegrationInstallResult.installed.join(", ") : "none")}`
26125
- );
26126
26047
  for (const warning of resolvedIntegrationInstallResult.warnings) {
26127
- installLines.push(` ${pc7.yellow("\u25B2")} ${warning}`);
26048
+ p18.log.warn(warning);
26128
26049
  }
26129
26050
  if (usesLocalStorage) {
26130
- installLines.push(
26131
- ` ${pc7.yellow("\u25B2")} Local filesystem storage is only for development or self-hosted persistent-disk deployments. Use Vercel Blob or Cloudflare R2 for hosted/serverless production.`
26051
+ p18.log.warn(
26052
+ "Local filesystem storage is only for development or self-hosted persistent-disk deployments. Use Vercel Blob or Cloudflare R2 for hosted/serverless production."
26132
26053
  );
26133
26054
  }
26134
- p18.note(installLines.join("\n"), "Installed");
26135
26055
  let dbPushed = false;
26136
26056
  if (depsResult.success && options.skipMigration && hasDbUrl(cwd)) {
26137
26057
  p18.log.info(`Skipping database schema push ${pc7.dim("(--skip-migration)")}`);
@@ -26160,29 +26080,35 @@ async function runInitCommand(name, options) {
26160
26080
  }
26161
26081
  if (driverReady) {
26162
26082
  const useTerminalForDbPush = shouldUseTerminalForDbPush(options);
26163
- if (useTerminalForDbPush) {
26164
- p18.log.info(`Running ${pc7.cyan("drizzle-kit push --force")}`);
26165
- } else {
26166
- s.start("Pushing database schema (drizzle-kit push)");
26167
- }
26168
- const pushResult = await runDrizzlePush(cwd, { interactive: useTerminalForDbPush });
26083
+ let dbSpinnerVisible = true;
26084
+ const clearDbSpinner = () => {
26085
+ if (!dbSpinnerVisible) return;
26086
+ dbSpinnerVisible = false;
26087
+ s.clear();
26088
+ };
26089
+ s.start(
26090
+ useTerminalForDbPush ? "Setting up database" : "Pushing database schema (drizzle-kit push)"
26091
+ );
26092
+ const pushResult = await runDrizzlePush(cwd, {
26093
+ interactive: useTerminalForDbPush,
26094
+ onOutput: clearDbSpinner
26095
+ });
26169
26096
  if (pushResult.success) {
26170
26097
  if (useTerminalForDbPush) {
26171
26098
  const verification = await verifyDatabaseReachable(cwd);
26099
+ clearDbSpinner();
26172
26100
  if (!verification.success) {
26173
26101
  p18.log.warning(verification.error);
26174
26102
  p18.log.error("Database was not reachable. Aborting setup.");
26175
26103
  process.exit(1);
26176
26104
  }
26177
- }
26178
- if (useTerminalForDbPush) {
26179
- p18.log.success("Database schema pushed");
26180
26105
  } else {
26181
- s.stop(`${pc7.green("\u2713")} Database schema pushed`);
26106
+ clearDbSpinner();
26182
26107
  }
26183
26108
  dbPushed = true;
26184
26109
  } else {
26185
- if (!useTerminalForDbPush) {
26110
+ if (dbSpinnerVisible) {
26111
+ dbSpinnerVisible = false;
26186
26112
  s.stop("Database push failed");
26187
26113
  }
26188
26114
  const pushError = pushResult.error ?? "Unknown error";
@@ -26208,7 +26134,9 @@ async function runInitCommand(name, options) {
26208
26134
  const adminDir = config.paths?.admin ?? "./admin";
26209
26135
  const authBasePath = `${resolveAdminNamespace(namespace).apiPath}/auth`;
26210
26136
  let replaceExistingAdmin = false;
26137
+ s.start("Setting up database");
26211
26138
  const adminCheck = await checkExistingAdmin(cwd, adminDir, authBasePath);
26139
+ s.clear();
26212
26140
  if (adminCheck.error) {
26213
26141
  p18.log.warning(`Could not verify existing admin account ${pc7.dim(`(${adminCheck.error})`)}`);
26214
26142
  if (isDatabaseReachabilityError(adminCheck.error)) {
@@ -26243,12 +26171,6 @@ async function runInitCommand(name, options) {
26243
26171
  }
26244
26172
  }
26245
26173
  if (!adminAccountReady) {
26246
- p18.note(
26247
- pc7.dim(
26248
- replaceExistingAdmin ? "Replace the existing admin account to access the admin." : "Create your first admin user to access the dashboard."
26249
- ),
26250
- "Admin account"
26251
- );
26252
26174
  const credentials = await p18.group(
26253
26175
  {
26254
26176
  email: () => p18.text({
@@ -26338,19 +26260,8 @@ Run manually: ${pc7.cyan(betterstartExecCommand(pm, "seed"))}`,
26338
26260
  s.stop("Git commit skipped");
26339
26261
  }
26340
26262
  }
26341
- const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
26342
26263
  const adminNamespace = resolveAdminNamespace(namespace);
26343
26264
  const adminLoginUrl = `http://localhost:${detectDevPort(cwd)}${adminNamespace.routePath}/login`;
26344
- const installedPluginList = resolvedPluginInstallResult.installed.length > 0 ? resolvedPluginInstallResult.installed.join(", ") : "core only";
26345
- const installedIntegrationList = resolvedIntegrationInstallResult.installed.length > 0 ? resolvedIntegrationInstallResult.installed.join(", ") : "none";
26346
- const summaryLines = [
26347
- `Plugins: ${pc7.cyan(installedPluginList)}`,
26348
- `Integrations: ${pc7.cyan(installedIntegrationList)}`,
26349
- `Storage: ${pc7.cyan(formatStorageProviderLabel(resolvedIntegrationInstallResult.config.storage.provider))}`,
26350
- `Files created: ${pc7.cyan(String(totalFiles))}`,
26351
- `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
26352
- ];
26353
- p18.note(summaryLines.join("\n"), "Admin scaffolded successfully");
26354
26265
  if (options.vercel !== false && !options.skipDeploy) {
26355
26266
  const linkedVercelProject = Boolean(readLinkedProjectId(cwd));
26356
26267
  if (!options.yes) {
@@ -26463,20 +26374,6 @@ function detectInstalledNextVersion(cwd) {
26463
26374
  return void 0;
26464
26375
  }
26465
26376
  }
26466
- function formatDevServerResult(result) {
26467
- switch (result.status) {
26468
- case "updated":
26469
- return `set to ${result.port}`;
26470
- case "preserved":
26471
- return `using ${result.port}`;
26472
- case "skipped":
26473
- return `custom script preserved; BetterStart uses ${result.port}`;
26474
- case "missing-script":
26475
- return `no dev script; BetterStart uses ${result.port}`;
26476
- case "missing-package":
26477
- return `package.json missing; BetterStart uses ${result.port}`;
26478
- }
26479
- }
26480
26377
  function shouldUseTerminalForDbPush(options) {
26481
26378
  return !options.yes && process.env.CI !== "true" && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
26482
26379
  }