betterstart-cli 0.0.92 → 0.0.94

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
@@ -52,6 +52,9 @@ async function loadConfigFile(configPath) {
52
52
  function isInteractiveSession() {
53
53
  return Boolean(process.stdin.isTTY) && process.env.CI !== "true";
54
54
  }
55
+ function isInteractiveTerminalSession() {
56
+ return isInteractiveSession() && Boolean(process.stdout.isTTY);
57
+ }
55
58
 
56
59
  // core-engine/commands/runtime.ts
57
60
  import { Argument, Command } from "commander";
@@ -120,7 +123,10 @@ function createInitCommand(runtime) {
120
123
  ).option("--database-provider <provider>", "Database provider: vercel, railway, or manual").option(
121
124
  "--storage-provider <provider>",
122
125
  "Storage provider: vercel-blob, railway-bucket, r2, or local"
123
- ).option("--deploy-provider <provider>", "Deploy provider: vercel, railway, or none").option("--database-plan <id>", "Neon plan id for a Vercel-provisioned database").option("--railway-workspace <id-or-name>", "Railway workspace for a new project").option("--railway-project <id-or-name>", "Existing Railway project to intentionally reuse").option("--railway-environment <id-or-name>", "Railway environment (default: production)").option("--railway-service <id-or-name>", "Railway app service (default: web)").option(
126
+ ).option("--deploy-provider <provider>", "Deploy provider: vercel, railway, or none").option(
127
+ "--use-existing-resources",
128
+ "Require database/storage credentials from existing project env files; never provision them"
129
+ ).option("--database-plan <id>", "Neon plan id for a Vercel-provisioned database").option("--railway-workspace <id-or-name>", "Railway workspace for a new project").option("--railway-project <id-or-name>", "Existing Railway project to intentionally reuse").option("--railway-environment <id-or-name>", "Railway environment (default: production)").option("--railway-service <id-or-name>", "Railway app service (default: web)").option(
124
130
  "--railway-bucket-region <region>",
125
131
  "Railway bucket region: sjc, iad, ams, or sin (default: iad)"
126
132
  ).option("--force", "Overwrite all existing Admin files (nuclear option)").addHelpText(
@@ -185,15 +191,18 @@ function createUpdateStylesCommand(runtime) {
185
191
  // core-engine/commands/default-action.ts
186
192
  var ADD_COMMAND = "add";
187
193
  var CREATE_COMMAND = "create";
194
+ var GENERATE_COMMAND = "generate";
188
195
  var INIT_COMMAND = "init";
189
196
  var REMOVE_COMMAND = "remove";
190
197
  var REMOVE_SCHEMA_COMMAND = "remove-schema";
198
+ var UPDATE_COMMAND = "update";
199
+ var ALL_FLAG = "--all";
191
200
  var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
192
201
  function cancelled(value) {
193
202
  return value === CANCELLED;
194
203
  }
195
204
  async function runDefaultAction(program2, runtime) {
196
- if (!isInteractiveSession()) {
205
+ if (!isInteractiveTerminalSession()) {
197
206
  program2.outputHelp();
198
207
  return;
199
208
  }
@@ -238,6 +247,24 @@ async function promptRequiredArguments(name, runtime, cwd) {
238
247
  if (name === ADD_COMMAND || name === REMOVE_COMMAND) {
239
248
  return promptInstallables(name, runtime, cwd);
240
249
  }
250
+ if (name === GENERATE_COMMAND) {
251
+ const schemas = await runtime.listSchemaChoices(cwd);
252
+ if (schemas.length === 0) {
253
+ p.log.warn("No schemas to generate.");
254
+ return CANCELLED;
255
+ }
256
+ const target = await p.select({
257
+ message: "What do you want to generate?",
258
+ options: [
259
+ ...schemas.map((value) => ({ value, label: value })),
260
+ { value: ALL_FLAG, label: "All schemas", hint: "regenerate everything" }
261
+ ]
262
+ });
263
+ return p.isCancel(target) ? CANCELLED : [target];
264
+ }
265
+ if (name === UPDATE_COMMAND) {
266
+ return promptComponents(runtime, cwd);
267
+ }
241
268
  if (name === REMOVE_SCHEMA_COMMAND) {
242
269
  const schemas = await runtime.listSchemaChoices(cwd);
243
270
  if (schemas.length === 0) {
@@ -252,6 +279,31 @@ async function promptRequiredArguments(name, runtime, cwd) {
252
279
  }
253
280
  return [];
254
281
  }
282
+ async function promptComponents(runtime, cwd) {
283
+ const scope = await p.select({
284
+ message: "What do you want to update?",
285
+ options: [
286
+ { value: ALL_FLAG, label: "All components", hint: "every installed component" },
287
+ { value: "pick", label: "Choose components" }
288
+ ]
289
+ });
290
+ if (p.isCancel(scope)) {
291
+ return CANCELLED;
292
+ }
293
+ if (scope === ALL_FLAG) {
294
+ return [ALL_FLAG];
295
+ }
296
+ const components = await runtime.listComponentChoices(cwd);
297
+ if (components.length === 0) {
298
+ p.log.warn("No components available to update.");
299
+ return CANCELLED;
300
+ }
301
+ const selected = await p.multiselect({
302
+ message: "Components to update",
303
+ options: components.map((value) => ({ value, label: value }))
304
+ });
305
+ return p.isCancel(selected) ? CANCELLED : selected;
306
+ }
255
307
  async function promptInstallables(name, runtime, cwd) {
256
308
  const removing = name === REMOVE_COMMAND;
257
309
  const choices = await runtime.listInstallableChoices(cwd);
@@ -1729,6 +1781,21 @@ function spawnAsync(cmd, args, cwd) {
1729
1781
  child.on("error", reject);
1730
1782
  });
1731
1783
  }
1784
+ function isUnexpectedPnpmStoreError(error) {
1785
+ return error instanceof Error && error.message.includes("ERR_PNPM_UNEXPECTED_STORE");
1786
+ }
1787
+ async function runDependencyInstall(pm, args, cwd, recovery) {
1788
+ try {
1789
+ await spawnAsync(pm, args, cwd);
1790
+ } catch (error) {
1791
+ if (pm !== "pnpm" || recovery.attempted || !isUnexpectedPnpmStoreError(error)) {
1792
+ throw error;
1793
+ }
1794
+ recovery.attempted = true;
1795
+ await spawnAsync("pnpm", ["install", "--force"], cwd);
1796
+ await spawnAsync(pm, args, cwd);
1797
+ }
1798
+ }
1732
1799
  function unique(values) {
1733
1800
  return Array.from(new Set(values));
1734
1801
  }
@@ -1775,11 +1842,22 @@ async function installDependenciesAsync({
1775
1842
  ensurePnpmAllowedBuilds(cwd, installPlan.pnpmAllowedBuilds ?? []);
1776
1843
  ensurePnpmPackageExtensions(cwd, installPlan.dependencies);
1777
1844
  }
1845
+ const pnpmStoreRecovery = { attempted: false };
1778
1846
  if (installPlan.dependencies.length > 0) {
1779
- await spawnAsync(pm, buildAddArgs(pm, installPlan.dependencies, false), cwd);
1847
+ await runDependencyInstall(
1848
+ pm,
1849
+ buildAddArgs(pm, installPlan.dependencies, false),
1850
+ cwd,
1851
+ pnpmStoreRecovery
1852
+ );
1780
1853
  }
1781
1854
  if (installPlan.devDependencies.length > 0) {
1782
- await spawnAsync(pm, buildAddArgs(pm, installPlan.devDependencies, true), cwd);
1855
+ await runDependencyInstall(
1856
+ pm,
1857
+ buildAddArgs(pm, installPlan.devDependencies, true),
1858
+ cwd,
1859
+ pnpmStoreRecovery
1860
+ );
1783
1861
  }
1784
1862
  return {
1785
1863
  dependencies: installPlan.dependencies,
@@ -3240,6 +3318,13 @@ function walkSlot(field, fieldPath, errors) {
3240
3318
  // core-engine/utils/env.ts
3241
3319
  import fs10 from "fs";
3242
3320
  import path12 from "path";
3321
+ import { parseEnv } from "util";
3322
+ var DEVELOPMENT_ENV_FILES = [
3323
+ ".env.development.local",
3324
+ ".env.local",
3325
+ ".env.development",
3326
+ ".env"
3327
+ ];
3243
3328
  function readEnvVar(cwd, key) {
3244
3329
  const envPath = path12.join(cwd, ".env.local");
3245
3330
  if (!fs10.existsSync(envPath)) {
@@ -3259,6 +3344,21 @@ function readEnvVar(cwd, key) {
3259
3344
  }
3260
3345
  return void 0;
3261
3346
  }
3347
+ function resolveProjectEnvVar(cwd, key) {
3348
+ for (const envFile of DEVELOPMENT_ENV_FILES) {
3349
+ const envPath = path12.join(cwd, envFile);
3350
+ if (!fs10.existsSync(envPath)) continue;
3351
+ try {
3352
+ const value = parseEnv(fs10.readFileSync(envPath, "utf-8"))[key]?.trim();
3353
+ if (value) return { source: envFile, value };
3354
+ } catch {
3355
+ }
3356
+ }
3357
+ return void 0;
3358
+ }
3359
+ function readProjectEnvVar(cwd, key) {
3360
+ return resolveProjectEnvVar(cwd, key)?.value;
3361
+ }
3262
3362
  function parseEnvValue(rawValue) {
3263
3363
  const value = rawValue.trim();
3264
3364
  const quote2 = value[0];
@@ -3645,10 +3745,10 @@ function hasRequiredIntegrationEnv(definition, cwd) {
3645
3745
  const requiredKeys = definition.envSections.flatMap(
3646
3746
  (section) => section.vars.filter((entry) => entry.value === "").map((entry) => entry.key)
3647
3747
  );
3648
- return requiredKeys.every((key) => Boolean(readEnvVar(cwd, key)));
3748
+ return requiredKeys.every((key) => Boolean(readProjectEnvVar(cwd, key)));
3649
3749
  }
3650
3750
  function readNonEmptyEnvVar(cwd, key) {
3651
- const value = readEnvVar(cwd, key)?.trim();
3751
+ const value = readProjectEnvVar(cwd, key)?.trim();
3652
3752
  return value ? value : void 0;
3653
3753
  }
3654
3754
  async function resolveTextEnvValue(options) {
@@ -4270,7 +4370,8 @@ async function installIntegrations({
4270
4370
  pm,
4271
4371
  integrationIds,
4272
4372
  interactive,
4273
- includeBiome
4373
+ includeBiome,
4374
+ dependenciesInstalled = false
4274
4375
  }) {
4275
4376
  const orderedIntegrationIds = resolveIntegrationInstallOrder(integrationIds);
4276
4377
  const installedIntegrationIds = normalizeInstalledIntegrations(config);
@@ -4348,7 +4449,7 @@ async function installIntegrations({
4348
4449
  (dependency) => !currentPlan.devDependencies.includes(dependency)
4349
4450
  )
4350
4451
  };
4351
- if (dependencyDiff.dependencies.length > 0 || dependencyDiff.devDependencies.length > 0) {
4452
+ if (!dependenciesInstalled && (dependencyDiff.dependencies.length > 0 || dependencyDiff.devDependencies.length > 0)) {
4352
4453
  const result = await installDependenciesAsync({
4353
4454
  cwd,
4354
4455
  pm,
@@ -19395,7 +19496,8 @@ async function installPresets({
19395
19496
  config,
19396
19497
  pm,
19397
19498
  presetIds,
19398
- includeBiome
19499
+ includeBiome,
19500
+ dependenciesInstalled = false
19399
19501
  }) {
19400
19502
  const orderedPresetIds = resolvePresetInstallOrder(presetIds);
19401
19503
  const installedPresetIds = normalizeInstalledPresets(config);
@@ -19430,7 +19532,7 @@ async function installPresets({
19430
19532
  (dependency) => !currentPlan.devDependencies.includes(dependency)
19431
19533
  )
19432
19534
  };
19433
- if (dependencyDiff.dependencies.length > 0 || dependencyDiff.devDependencies.length > 0) {
19535
+ if (!dependenciesInstalled && (dependencyDiff.dependencies.length > 0 || dependencyDiff.devDependencies.length > 0)) {
19434
19536
  const result = await installDependenciesAsync({
19435
19537
  cwd,
19436
19538
  pm,
@@ -22785,7 +22887,7 @@ async function promptPresets(cwd, options = {}) {
22785
22887
  mergeIntegrationConfig(await collectIntegrationConfig(cwd, ["railway-bucket"]));
22786
22888
  }
22787
22889
  } else if (storage === "vercel-blob") {
22788
- const existingToken = readEnvVar(cwd, "BLOB_READ_WRITE_TOKEN")?.trim();
22890
+ const existingToken = readProjectEnvVar(cwd, "BLOB_READ_WRITE_TOKEN")?.trim();
22789
22891
  const flow = !existingToken && options.provisionVercelBlob ? await options.provisionVercelBlob() : void 0;
22790
22892
  if (flow?.ok && flow.token) {
22791
22893
  persistBlobReadWriteToken(cwd, flow.token);
@@ -22898,6 +23000,14 @@ function validateInitProviderFlags(options) {
22898
23000
  if (options.databasePlan && options.databaseProvider && options.databaseProvider !== "vercel") {
22899
23001
  throw new Error("--database-plan can only be used with --database-provider vercel.");
22900
23002
  }
23003
+ if (options.useExistingResources && !options.yes) {
23004
+ throw new Error("--use-existing-resources requires --yes.");
23005
+ }
23006
+ if (options.useExistingResources && options.databaseProvider && options.databaseProvider !== "manual") {
23007
+ throw new Error(
23008
+ "--use-existing-resources requires --database-provider manual for an existing DATABASE_URL."
23009
+ );
23010
+ }
22901
23011
  }
22902
23012
  function resolveFlagStorageProvider(explicitProvider, integrations) {
22903
23013
  const inferred = Array.from(
@@ -25055,8 +25165,16 @@ function scaffoldLayout({ cwd, config }) {
25055
25165
  readTemplate("pages/settings/forms/forms-settings-page-content.tsx")
25056
25166
  );
25057
25167
  write(
25058
- path43.join(settingsFormsDir, "edit-form-notifications-dialog.tsx"),
25059
- readTemplate("pages/settings/forms/edit-form-notifications-dialog.tsx")
25168
+ path43.join(settingsFormsDir, "forms-settings-columns.tsx"),
25169
+ readTemplate("pages/settings/forms/forms-settings-columns.tsx")
25170
+ );
25171
+ write(
25172
+ path43.join(settingsFormsDir, "forms-settings-table.tsx"),
25173
+ readTemplate("pages/settings/forms/forms-settings-table.tsx")
25174
+ );
25175
+ write(
25176
+ path43.join(settingsFormsDir, "form-notifications-drawer.tsx"),
25177
+ readTemplate("pages/settings/forms/form-notifications-drawer.tsx")
25060
25178
  );
25061
25179
  const settingsWebhooksDir = path43.join(settingsDir, "webhooks");
25062
25180
  write(
@@ -27436,20 +27554,62 @@ function removeExistingAdminPaths(cwd, namespaces) {
27436
27554
  }
27437
27555
  return removed;
27438
27556
  }
27557
+ function writeInitJson(context, payload) {
27558
+ context.restoreStdout?.();
27559
+ context.restoreStdout = void 0;
27560
+ context.written = true;
27561
+ console.log(JSON.stringify(payload, null, 2));
27562
+ }
27439
27563
  async function runInitCommand(name, options) {
27440
- let restoreStdout;
27564
+ const jsonContext = {
27565
+ restoreStdout: options.json ? redirectStdoutToStderr() : void 0,
27566
+ written: false
27567
+ };
27568
+ try {
27569
+ await runInitCommandInternal(name, options, jsonContext);
27570
+ } catch (error) {
27571
+ if (options.json && !jsonContext.written) {
27572
+ const message = error instanceof Error ? error.message : String(error);
27573
+ writeInitJson(jsonContext, {
27574
+ success: false,
27575
+ phase: "initialization",
27576
+ error: {
27577
+ code: "UNEXPECTED_INIT_ERROR",
27578
+ message: redactSecrets(message)
27579
+ }
27580
+ });
27581
+ }
27582
+ throw error;
27583
+ } finally {
27584
+ jsonContext.restoreStdout?.();
27585
+ }
27586
+ }
27587
+ async function runInitCommandInternal(name, options, jsonContext) {
27588
+ const exitInit = (phase, message, code = "INIT_FAILED") => {
27589
+ if (options.json) {
27590
+ writeInitJson(jsonContext, {
27591
+ success: false,
27592
+ phase,
27593
+ error: {
27594
+ code,
27595
+ message: redactSecrets(message)
27596
+ }
27597
+ });
27598
+ }
27599
+ process.exit(1);
27600
+ };
27441
27601
  if (options.json) {
27442
27602
  if (!options.yes) {
27443
- p26.log.error("--json requires --yes.");
27444
- process.exit(1);
27603
+ const message = "--json requires --yes.";
27604
+ p26.log.error(message);
27605
+ exitInit("validation", message, "JSON_REQUIRES_YES");
27445
27606
  }
27446
- restoreStdout = redirectStdoutToStderr();
27447
27607
  }
27448
27608
  installPromptCheckmarks();
27449
27609
  const disposeCancelGuard = installSetupCancelGuard();
27450
27610
  renderInitBanner();
27451
- let selectedPresets;
27452
- let selectedIntegrations;
27611
+ let selectedPresets = [];
27612
+ let selectedIntegrations = [];
27453
27613
  let flagStorageProvider;
27454
27614
  try {
27455
27615
  validateInitProviderFlags(options);
@@ -27461,8 +27621,9 @@ async function runInitCommand(name, options) {
27461
27621
  }
27462
27622
  } catch (error) {
27463
27623
  disposeCancelGuard();
27464
- p26.log.error(error instanceof Error ? error.message : String(error));
27465
- process.exit(1);
27624
+ const message = error instanceof Error ? error.message : String(error);
27625
+ p26.log.error(message);
27626
+ exitInit("validation", message, "INVALID_OPTIONS");
27466
27627
  }
27467
27628
  let cwd = process.cwd();
27468
27629
  let projectName = path52.basename(cwd);
@@ -27472,8 +27633,9 @@ async function runInitCommand(name, options) {
27472
27633
  try {
27473
27634
  namespace = validateAdminNamespace(options.namespace);
27474
27635
  } catch (error) {
27475
- p26.log.error(error instanceof Error ? error.message : String(error));
27476
- process.exit(1);
27636
+ const message = error instanceof Error ? error.message : String(error);
27637
+ p26.log.error(message);
27638
+ exitInit("validation", message, "INVALID_NAMESPACE");
27477
27639
  }
27478
27640
  }
27479
27641
  let projectPrompt;
@@ -27481,8 +27643,9 @@ async function runInitCommand(name, options) {
27481
27643
  try {
27482
27644
  projectPrompt = options.yes ? resolveNonInteractiveProject(name) : await promptProject(name);
27483
27645
  } catch (error) {
27484
- p26.log.error(error instanceof Error ? error.message : String(error));
27485
- process.exit(1);
27646
+ const message = error instanceof Error ? error.message : String(error);
27647
+ p26.log.error(message);
27648
+ exitInit("project", message, "PROJECT_SETUP_FAILED");
27486
27649
  }
27487
27650
  }
27488
27651
  if (!options.yes && !options.namespace) {
@@ -27513,8 +27676,9 @@ async function runInitCommand(name, options) {
27513
27676
  if (project2.isExisting) {
27514
27677
  srcDir = project2.hasSrcDir;
27515
27678
  if (!project2.hasTypeScript) {
27516
- p26.log.error("TypeScript is required. Please add a tsconfig.json first.");
27517
- process.exit(1);
27679
+ const message = "TypeScript is required. Please add a tsconfig.json first.";
27680
+ p26.log.error(message);
27681
+ exitInit("project", message, "TYPESCRIPT_REQUIRED");
27518
27682
  }
27519
27683
  if (forceMode) {
27520
27684
  const nuked = removeExistingAdminPaths(cwd, await resolveForceInitNamespaces(cwd, namespace));
@@ -27530,10 +27694,9 @@ async function runInitCommand(name, options) {
27530
27694
  );
27531
27695
  p26.note(conflictLines.join("\n"), pc10.yellow("Conflicts"));
27532
27696
  if (options.yes) {
27533
- p26.log.error(
27534
- "Can't continue with --yes while admin files conflict. Re-run with --force to remove them first."
27535
- );
27536
- process.exit(1);
27697
+ const message = "Can't continue with --yes while admin files conflict. Re-run with --force to remove them first.";
27698
+ p26.log.error(message);
27699
+ exitInit("project", message, "ADMIN_FILES_CONFLICT");
27537
27700
  }
27538
27701
  const proceed = await p26.confirm({
27539
27702
  message: [
@@ -27608,7 +27771,7 @@ async function runInitCommand(name, options) {
27608
27771
  ${pc10.cyan(`npx create-next-app@latest ${freshProject.projectName} --typescript --tailwind --app`)}
27609
27772
  Then run ${pc10.cyan("betterstart init")} inside it.`
27610
27773
  );
27611
- process.exit(1);
27774
+ exitInit("project", createNextAppResult.error, "CREATE_NEXT_APP_FAILED");
27612
27775
  }
27613
27776
  cwd = path52.resolve(cwd, freshProject.projectName);
27614
27777
  const hasPackageJson = fs41.existsSync(path52.join(cwd, "package.json"));
@@ -27626,7 +27789,11 @@ async function runInitCommand(name, options) {
27626
27789
  ${pc10.cyan(manualCmd)}
27627
27790
  Then run ${pc10.cyan("betterstart init")} inside it.`
27628
27791
  );
27629
- process.exit(1);
27792
+ exitInit(
27793
+ "project",
27794
+ "create-next-app completed but the project was not created.",
27795
+ "CREATE_NEXT_APP_INCOMPLETE"
27796
+ );
27630
27797
  }
27631
27798
  createNextAppSpinner.clear();
27632
27799
  project2 = detectProject(cwd, namespace);
@@ -27638,7 +27805,8 @@ async function runInitCommand(name, options) {
27638
27805
  let railwayDatabaseServiceName;
27639
27806
  let railwayBucketResourceName;
27640
27807
  let railwayBucketUrlStyle;
27641
- const existingDbUrl = readExistingDbUrl(cwd);
27808
+ const existingDatabase = readExistingDbUrl(cwd);
27809
+ const existingDbUrl = existingDatabase?.value;
27642
27810
  const getRailwaySession = () => {
27643
27811
  railwaySessionPromise ??= createRailwaySession({
27644
27812
  cwd,
@@ -27670,24 +27838,28 @@ async function runInitCommand(name, options) {
27670
27838
  try {
27671
27839
  validateResolvedDatabaseProvider(databaseProvider, options);
27672
27840
  } catch (error) {
27673
- p26.log.error(error instanceof Error ? error.message : String(error));
27674
- process.exit(1);
27841
+ const message = error instanceof Error ? error.message : String(error);
27842
+ p26.log.error(message);
27843
+ exitInit("providers", message, "INVALID_DATABASE_PROVIDER");
27675
27844
  }
27676
27845
  if (databaseProvider === "manual") {
27677
27846
  const candidate = options.databaseUrl ?? existingDbUrl ?? promptedManualUrl;
27678
27847
  if (candidate && !isValidDbUrl(candidate)) {
27679
- p26.log.error(
27680
- `Invalid database URL. Must start with ${pc10.cyan("postgres://")} or ${pc10.cyan("postgresql://")}`
27681
- );
27682
- process.exit(1);
27848
+ const message = "Invalid database URL. Must start with postgres:// or postgresql://";
27849
+ p26.log.error(message);
27850
+ exitInit("providers", message, "INVALID_DATABASE_URL");
27683
27851
  }
27684
27852
  if (candidate) {
27685
27853
  databaseUrl = candidate;
27686
27854
  if (existingDbUrl === candidate && !options.databaseUrl) {
27687
27855
  p26.log.info(
27688
- `Using the existing DATABASE_URL from .env.local ${pc10.dim(`(${maskDbUrl(candidate)})`)}`
27856
+ `Using the existing DATABASE_URL from ${existingDatabase?.source ?? "project env"} ${pc10.dim(`(${maskDbUrl(candidate)})`)}`
27689
27857
  );
27690
27858
  }
27859
+ } else if (options.useExistingResources) {
27860
+ const message = "DATABASE_URL is missing from the project environment files.";
27861
+ p26.log.error(message);
27862
+ exitInit("providers", message, "MISSING_DATABASE_URL");
27691
27863
  } else if (!options.yes) {
27692
27864
  databaseUrl = await promptConnectionString();
27693
27865
  }
@@ -27704,10 +27876,9 @@ async function runInitCommand(name, options) {
27704
27876
  persistDatabaseUrl(cwd, databaseUrl);
27705
27877
  dismissVercelSignedInNote = flow.dismissSignedInNote;
27706
27878
  } else if (options.yes) {
27707
- p26.log.error(
27708
- flow.ok ? "Created a Neon database, but DATABASE_URL could not be retrieved from Vercel." : "Vercel database provisioning did not complete."
27709
- );
27710
- process.exit(1);
27879
+ const message = flow.ok ? "Created a Neon database, but DATABASE_URL could not be retrieved from Vercel." : "Vercel database provisioning did not complete.";
27880
+ p26.log.error(message);
27881
+ exitInit("providers", message, "DATABASE_PROVISIONING_FAILED");
27711
27882
  } else if (flow.ok) {
27712
27883
  openBrowserVercelNeonResource(flow.dashboardUrl ?? flow.resourceUrl);
27713
27884
  databaseUrl = await promptConnectionString();
@@ -27727,8 +27898,9 @@ async function runInitCommand(name, options) {
27727
27898
  } catch (error) {
27728
27899
  const message = error instanceof Error ? error.message : String(error);
27729
27900
  if (options.yes) {
27730
- p26.log.error(`Railway database provisioning failed: ${message}`);
27731
- process.exit(1);
27901
+ const failureMessage = `Railway database provisioning failed: ${message}`;
27902
+ p26.log.error(failureMessage);
27903
+ exitInit("providers", failureMessage, "DATABASE_PROVISIONING_FAILED");
27732
27904
  }
27733
27905
  p26.log.warn(`Railway database provisioning failed: ${message}`);
27734
27906
  p26.log.info("Falling back to a manual database connection string.");
@@ -27773,27 +27945,39 @@ async function runInitCommand(name, options) {
27773
27945
  storage
27774
27946
  };
27775
27947
  if (storage === "r2") {
27776
- const missingR2Keys = R2_ENV_KEYS.filter((key) => !readEnvVar(cwd, key)?.trim());
27948
+ const missingR2Keys = R2_ENV_KEYS.filter((key) => !readProjectEnvVar(cwd, key)?.trim());
27777
27949
  if (options.yes && missingR2Keys.length > 0) {
27778
- p26.log.error(
27779
- `Cloudflare R2 is missing required environment variables: ${missingR2Keys.join(", ")}.`
27780
- );
27781
- process.exit(1);
27950
+ const message = `Cloudflare R2 is missing required environment variables: ${missingR2Keys.join(", ")}.`;
27951
+ p26.log.error(message);
27952
+ exitInit("providers", message, "MISSING_STORAGE_CREDENTIALS");
27782
27953
  }
27783
27954
  mergeCollectedIntegrationConfig(await collectIntegrationConfig(cwd, ["r2"]));
27784
27955
  } else if (storage === "railway-bucket") {
27785
27956
  if (hasRailwayBucketConfig(cwd)) {
27786
27957
  mergeCollectedIntegrationConfig(await collectIntegrationConfig(cwd, ["railway-bucket"]));
27958
+ } else if (options.useExistingResources) {
27959
+ const missingKeys = RAILWAY_BUCKET_ENV_KEYS.filter(
27960
+ (key) => !readProjectEnvVar(cwd, key)?.trim()
27961
+ );
27962
+ const message = `Railway Bucket is missing required environment variables: ${missingKeys.join(", ")}.`;
27963
+ p26.log.error(message);
27964
+ exitInit("providers", message, "MISSING_STORAGE_CREDENTIALS");
27787
27965
  } else {
27788
27966
  const flow = await provisionRailwayBucket();
27789
27967
  if (flow.ok && flow.config) {
27790
27968
  mergeCollectedIntegrationConfig(flow.config);
27791
27969
  } else if (options.yes) {
27792
- p26.log.error("Railway bucket provisioning did not complete.");
27793
- process.exit(1);
27970
+ const message = "Railway bucket provisioning did not complete.";
27971
+ p26.log.error(message);
27972
+ exitInit("providers", message, "STORAGE_PROVISIONING_FAILED");
27794
27973
  }
27795
27974
  }
27796
- } else if (storage === "vercel-blob" && !readEnvVar(cwd, "BLOB_READ_WRITE_TOKEN")?.trim()) {
27975
+ } else if (storage === "vercel-blob" && !readProjectEnvVar(cwd, "BLOB_READ_WRITE_TOKEN")?.trim()) {
27976
+ if (options.useExistingResources) {
27977
+ const message = "BLOB_READ_WRITE_TOKEN is missing from the project environment files.";
27978
+ p26.log.error(message);
27979
+ exitInit("providers", message, "MISSING_STORAGE_CREDENTIALS");
27980
+ }
27797
27981
  const flow = await runVercelBlobFlow({
27798
27982
  cwd,
27799
27983
  projectName,
@@ -27808,8 +27992,9 @@ async function runInitCommand(name, options) {
27808
27992
  });
27809
27993
  collectedIntegrationConfig.overwriteKeys.add("BLOB_READ_WRITE_TOKEN");
27810
27994
  } else if (options.yes) {
27811
- p26.log.error("Vercel Blob provisioning did not complete.");
27812
- process.exit(1);
27995
+ const message = "Vercel Blob provisioning did not complete.";
27996
+ p26.log.error(message);
27997
+ exitInit("providers", message, "STORAGE_PROVISIONING_FAILED");
27813
27998
  } else {
27814
27999
  p26.log.warn(
27815
28000
  "Continuing without a Blob token \u2014 add BLOB_READ_WRITE_TOKEN to .env.local from the store on vercel.com/dashboard/stores."
@@ -27846,6 +28031,45 @@ async function runInitCommand(name, options) {
27846
28031
  config.paths = config.frameworkConfig.next.paths;
27847
28032
  config.database.migrationsDir = deriveMigrationsDir(namespace);
27848
28033
  const s = spinner2();
28034
+ const coreDependencyPlan = getDependencyPlan(
28035
+ presetSelection.presets,
28036
+ presetSelection.integrations,
28037
+ project2.linter.type === "none"
28038
+ );
28039
+ const cliDependencyPlan = getCliDependencySyncPlan(cwd);
28040
+ s.start("Installing dependencies, This may take a moment");
28041
+ const depsResult = await installDependenciesAsync({
28042
+ cwd,
28043
+ pm,
28044
+ ...isFreshProject && pm === "pnpm" ? { pnpmAllowedBuilds: [...FRESH_PNPM_ALLOWED_BUILDS] } : {},
28045
+ dependencies: Array.from(
28046
+ /* @__PURE__ */ new Set([...coreDependencyPlan.dependencies, ...cliDependencyPlan?.dependencies ?? []])
28047
+ ),
28048
+ devDependencies: Array.from(
28049
+ /* @__PURE__ */ new Set([
28050
+ ...coreDependencyPlan.devDependencies,
28051
+ ...cliDependencyPlan?.devDependencies ?? []
28052
+ ])
28053
+ )
28054
+ });
28055
+ if (depsResult.success) {
28056
+ s.stop("");
28057
+ } else {
28058
+ const message = redactSecrets(depsResult.error ?? "Unknown dependency installation error");
28059
+ s.stop("Failed to install dependencies");
28060
+ p26.log.warn(message);
28061
+ p26.log.info(
28062
+ `You can install them manually:
28063
+ ${pc10.cyan(`${pm} add ${depsResult.dependencies.join(" ")}`)}
28064
+ ${pc10.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
28065
+ );
28066
+ disposeCancelGuard();
28067
+ exitInit(
28068
+ "dependencies",
28069
+ message,
28070
+ message.includes("ERR_PNPM_UNEXPECTED_STORE") ? "PNPM_UNEXPECTED_STORE" : "DEPENDENCY_INSTALL_FAILED"
28071
+ );
28072
+ }
27849
28073
  s.start("Directory structure");
27850
28074
  scaffoldBase({
27851
28075
  cwd,
@@ -27913,36 +28137,6 @@ async function runInitCommand(name, options) {
27913
28137
  }
27914
28138
  }
27915
28139
  }
27916
- const coreDependencyPlan = getDependencyPlan([], [], project2.linter.type === "none");
27917
- const cliDependencyPlan = getCliDependencySyncPlan(cwd);
27918
- s.start("Installing dependencies, This may take a moment");
27919
- const depsResult = await installDependenciesAsync({
27920
- cwd,
27921
- pm,
27922
- ...isFreshProject && pm === "pnpm" ? { pnpmAllowedBuilds: [...FRESH_PNPM_ALLOWED_BUILDS] } : {},
27923
- dependencies: Array.from(
27924
- /* @__PURE__ */ new Set([...coreDependencyPlan.dependencies, ...cliDependencyPlan?.dependencies ?? []])
27925
- ),
27926
- devDependencies: Array.from(
27927
- /* @__PURE__ */ new Set([
27928
- ...coreDependencyPlan.devDependencies,
27929
- ...cliDependencyPlan?.devDependencies ?? []
27930
- ])
27931
- )
27932
- });
27933
- if (depsResult.success) {
27934
- s.stop("");
27935
- } else {
27936
- s.stop("Failed to install dependencies");
27937
- p26.log.warn(depsResult.error ?? "Unknown error");
27938
- p26.log.info(
27939
- `You can install them manually:
27940
- ${pc10.cyan(`${pm} add ${depsResult.dependencies.join(" ")}`)}
27941
- ${pc10.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
27942
- );
27943
- disposeCancelGuard();
27944
- process.exit(1);
27945
- }
27946
28140
  process.stdout.write("\x1B[2A\x1B[J");
27947
28141
  s.start("Generating core schemas");
27948
28142
  const coreSchemasResult = scaffoldCoreSchemas({ cwd, config });
@@ -27956,7 +28150,8 @@ async function runInitCommand(name, options) {
27956
28150
  pm,
27957
28151
  presetIds: presetSelection.presets,
27958
28152
  interactive: !options.yes,
27959
- includeBiome: project2.linter.type === "none"
28153
+ includeBiome: project2.linter.type === "none",
28154
+ dependenciesInstalled: true
27960
28155
  });
27961
28156
  })() : Promise.resolve({ installed: [], skipped: [], warnings: [], config });
27962
28157
  const resolvedPresetInstallResult = await presetInstallResult;
@@ -27975,7 +28170,8 @@ async function runInitCommand(name, options) {
27975
28170
  // .env.local, so the install runs without prompting (rule: no
27976
28171
  // mid-scaffold input).
27977
28172
  interactive: false,
27978
- includeBiome: project2.linter.type === "none"
28173
+ includeBiome: project2.linter.type === "none",
28174
+ dependenciesInstalled: true
27979
28175
  });
27980
28176
  })() : Promise.resolve({
27981
28177
  installed: [],
@@ -28051,8 +28247,9 @@ async function runInitCommand(name, options) {
28051
28247
  clearDbSpinner();
28052
28248
  if (!verification.success) {
28053
28249
  p26.log.warn(verification.error);
28054
- p26.log.error("Database was not reachable. Aborting setup.");
28055
- process.exit(1);
28250
+ const message = "Database was not reachable. Aborting setup.";
28251
+ p26.log.error(message);
28252
+ exitInit("database", verification.error, "DATABASE_UNREACHABLE");
28056
28253
  }
28057
28254
  } else {
28058
28255
  clearDbSpinner();
@@ -28066,8 +28263,9 @@ async function runInitCommand(name, options) {
28066
28263
  const pushError = pushResult.error ?? "Unknown error";
28067
28264
  p26.log.warn(pushError);
28068
28265
  if (isDatabaseReachabilityError(pushError)) {
28069
- p26.log.error("Database was not reachable. Aborting setup.");
28070
- process.exit(1);
28266
+ const message = "Database was not reachable. Aborting setup.";
28267
+ p26.log.error(message);
28268
+ exitInit("database", pushError, "DATABASE_UNREACHABLE");
28071
28269
  }
28072
28270
  p26.log.info(`You can run it manually: ${pc10.cyan(drizzlePushCommand(pm))}`);
28073
28271
  }
@@ -28092,8 +28290,9 @@ async function runInitCommand(name, options) {
28092
28290
  if (adminCheck.error) {
28093
28291
  p26.log.warn(`Could not verify existing admin account ${pc10.dim(`(${adminCheck.error})`)}`);
28094
28292
  if (isDatabaseReachabilityError(adminCheck.error)) {
28095
- p26.log.error("Database was not reachable. Aborting setup.");
28096
- process.exit(1);
28293
+ const message = "Database was not reachable. Aborting setup.";
28294
+ p26.log.error(message);
28295
+ exitInit("database", adminCheck.error, "DATABASE_UNREACHABLE");
28097
28296
  }
28098
28297
  } else if (adminCheck.existingAdmin) {
28099
28298
  const existingAdminLabel = formatAdminIdentity(adminCheck.existingAdmin);
@@ -28202,8 +28401,9 @@ Run manually: ${pc10.cyan(betterstartExecCommand(pm, "seed"))}`,
28202
28401
  pc10.red("Seed failed")
28203
28402
  );
28204
28403
  if (isDatabaseReachabilityError(seedResult.error)) {
28205
- p26.log.error("Database was not reachable. Aborting setup.");
28206
- process.exit(1);
28404
+ const message = "Database was not reachable. Aborting setup.";
28405
+ p26.log.error(message);
28406
+ exitInit("database", seedResult.error, "DATABASE_UNREACHABLE");
28207
28407
  }
28208
28408
  }
28209
28409
  }
@@ -28244,8 +28444,9 @@ Run manually: ${pc10.cyan(betterstartExecCommand(pm, "seed"))}`,
28244
28444
  deployedUrl = deployFlow.url;
28245
28445
  if (!deployFlow.ok) {
28246
28446
  if (options.yes) {
28247
- p26.log.error("Vercel deploy did not complete.");
28248
- process.exit(1);
28447
+ const message = "Vercel deploy did not complete.";
28448
+ p26.log.error(message);
28449
+ exitInit("deployment", message, "DEPLOYMENT_FAILED");
28249
28450
  }
28250
28451
  p26.log.warn("Vercel deploy did not complete; continuing.");
28251
28452
  }
@@ -28267,16 +28468,18 @@ Run manually: ${pc10.cyan(betterstartExecCommand(pm, "seed"))}`,
28267
28468
  if (!deployFlow.ok) {
28268
28469
  if (deployFlow.detail) p26.log.message(pc10.dim(redactSecrets(deployFlow.detail)));
28269
28470
  if (options.yes) {
28270
- p26.log.error("Railway deploy did not complete.");
28271
- process.exit(1);
28471
+ const message = "Railway deploy did not complete.";
28472
+ p26.log.error(message);
28473
+ exitInit("deployment", message, "DEPLOYMENT_FAILED");
28272
28474
  }
28273
28475
  p26.log.warn("Railway deploy did not complete; continuing.");
28274
28476
  }
28275
28477
  } catch (error) {
28276
28478
  const message = error instanceof Error ? error.message : String(error);
28277
28479
  if (options.yes) {
28278
- p26.log.error(`Railway deploy failed: ${message}`);
28279
- process.exit(1);
28480
+ const failureMessage = `Railway deploy failed: ${message}`;
28481
+ p26.log.error(failureMessage);
28482
+ exitInit("deployment", failureMessage, "DEPLOYMENT_FAILED");
28280
28483
  }
28281
28484
  p26.log.warn(`Railway deploy failed: ${message}`);
28282
28485
  }
@@ -28297,26 +28500,20 @@ Run manually: ${pc10.cyan(betterstartExecCommand(pm, "seed"))}`,
28297
28500
  }
28298
28501
  }
28299
28502
  disposeCancelGuard();
28300
- if (options.json && restoreStdout) {
28301
- restoreStdout();
28302
- console.log(
28303
- JSON.stringify(
28304
- {
28305
- namespace,
28306
- configPath: CONFIG_FILE_NAME,
28307
- databaseProvider,
28308
- storageProvider: presetSelection.storage,
28309
- deployProvider,
28310
- presets: presetSelection.presets,
28311
- integrations: presetSelection.integrations,
28312
- adminUrl: adminLoginUrl,
28313
- adminEmail: seedEmail ?? null,
28314
- deployedUrl: deployedUrl ?? null
28315
- },
28316
- null,
28317
- 2
28318
- )
28319
- );
28503
+ if (options.json) {
28504
+ writeInitJson(jsonContext, {
28505
+ success: true,
28506
+ namespace,
28507
+ configPath: CONFIG_FILE_NAME,
28508
+ databaseProvider,
28509
+ storageProvider: presetSelection.storage,
28510
+ deployProvider,
28511
+ presets: presetSelection.presets,
28512
+ integrations: presetSelection.integrations,
28513
+ adminUrl: adminLoginUrl,
28514
+ adminEmail: seedEmail ?? null,
28515
+ deployedUrl: deployedUrl ?? null
28516
+ });
28320
28517
  return;
28321
28518
  }
28322
28519
  p26.outro(`Admin ready at ${adminNamespace.routePath}`);
@@ -28325,21 +28522,13 @@ function isValidDbUrl(url) {
28325
28522
  return url.startsWith("postgres://") || url.startsWith("postgresql://");
28326
28523
  }
28327
28524
  function readExistingDbUrl(cwd) {
28328
- const envPath = path52.join(cwd, ".env.local");
28329
- if (!fs41.existsSync(envPath)) return void 0;
28330
- const content = fs41.readFileSync(envPath, "utf-8");
28331
- for (const line of content.split("\n")) {
28332
- const trimmed = line.trim();
28333
- if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
28334
- const [key, ...rest] = trimmed.split("=");
28335
- if (key?.trim() === "DATABASE_URL") {
28336
- const val = rest.join("=").replace(/^['"]|['"]$/g, "").trim();
28337
- if (val.length > 0 && !val.startsWith("your_") && val !== "postgresql://..." && isValidDbUrl(val)) {
28338
- return val;
28339
- }
28340
- }
28525
+ const resolved = resolveProjectEnvVar(cwd, "DATABASE_URL");
28526
+ if (!resolved) return void 0;
28527
+ const value = resolved.value.trim();
28528
+ if (value.startsWith("your_") || value === "postgresql://..." || !isValidDbUrl(value)) {
28529
+ return void 0;
28341
28530
  }
28342
- return void 0;
28531
+ return { ...resolved, value };
28343
28532
  }
28344
28533
  function maskDbUrl(url) {
28345
28534
  try {
@@ -28381,7 +28570,7 @@ var R2_ENV_KEYS = [
28381
28570
  "BETTERSTART_R2_PUBLIC_URL"
28382
28571
  ];
28383
28572
  function hasRailwayBucketConfig(cwd) {
28384
- return RAILWAY_BUCKET_ENV_KEYS.every((key) => Boolean(readEnvVar(cwd, key)?.trim()));
28573
+ return RAILWAY_BUCKET_ENV_KEYS.every((key) => Boolean(readProjectEnvVar(cwd, key)?.trim()));
28385
28574
  }
28386
28575
  function railwayBucketIntegrationConfig(credentials) {
28387
28576
  return {
@@ -28501,7 +28690,7 @@ main().catch((error) => {
28501
28690
  cwd,
28502
28691
  timeoutMs: 15e3,
28503
28692
  env: {
28504
- DATABASE_URL: databaseUrl
28693
+ DATABASE_URL: databaseUrl.value
28505
28694
  }
28506
28695
  }
28507
28696
  );
@@ -28914,581 +29103,13 @@ async function runListPresetsCommand(options) {
28914
29103
  }
28915
29104
 
28916
29105
  // adapters/next/commands/menu-choices.ts
28917
- import path55 from "path";
28918
- async function listInstallableChoices(cwd) {
28919
- const config = await resolveConfigOrExit(cwd);
28920
- const installedPresets = new Set(config.presets.installed);
28921
- const installedIntegrations = new Set(config.integrations.installed);
28922
- return {
28923
- presets: listAvailablePresets().map((preset) => ({
28924
- id: preset.id,
28925
- description: preset.description,
28926
- installed: installedPresets.has(preset.id)
28927
- })),
28928
- integrations: listAvailableIntegrations().map((integration) => ({
28929
- id: integration.id,
28930
- description: integration.description,
28931
- installed: installedIntegrations.has(integration.id)
28932
- }))
28933
- };
28934
- }
28935
- async function listSchemaChoices(cwd) {
28936
- const config = await resolveConfigOrExit(cwd);
28937
- const paths = resolveProjectPaths(config);
28938
- return listSchemaNames(path55.join(cwd, ...paths.schemasDir.split("/")));
28939
- }
28940
-
28941
- // adapters/next/commands/remove.ts
28942
29106
  import path56 from "path";
28943
- import * as p29 from "@clack/prompts";
28944
- async function runRemoveCommand(items, options) {
28945
- const removeIntegrationsMode = Boolean(options.integration);
28946
- if (!removeIntegrationsMode && items.includes("core")) {
28947
- p29.log.error("The core Admin cannot be removed.");
28948
- process.exit(1);
28949
- }
28950
- const presetIds = items.filter(isPresetId);
28951
- const integrationIds = items.filter(isIntegrationId);
28952
- if (!removeIntegrationsMode && integrationIds.length > 0) {
28953
- p29.log.error(
28954
- `Integration IDs require --integration. Run \`betterstart remove --integration ${integrationIds.join(" ")}\`.`
28955
- );
28956
- process.exit(1);
28957
- }
28958
- if (removeIntegrationsMode && presetIds.length > 0) {
28959
- p29.log.error(`Preset IDs cannot be removed with --integration: ${presetIds.join(", ")}`);
28960
- process.exit(1);
28961
- }
28962
- const invalidItems = removeIntegrationsMode ? items.filter((integrationId) => !isIntegrationId(integrationId)) : items.filter((presetId) => !isPresetId(presetId));
28963
- if (invalidItems.length > 0) {
28964
- p29.log.error(
28965
- removeIntegrationsMode ? formatUnknownIntegrationMessage(invalidItems) : formatUnknownPresetMessage(invalidItems)
28966
- );
28967
- process.exit(1);
28968
- }
28969
- const cwd = options.cwd ? path56.resolve(options.cwd) : process.cwd();
28970
- const config = await resolveConfigOrExit(cwd);
28971
- const pm = detectPackageManager(cwd);
28972
- if (!options.force) {
28973
- const confirmed = await p29.confirm({
28974
- message: `Remove ${removeIntegrationsMode ? "integration" : "preset"}${items.length === 1 ? "" : "s"} ${items.join(", ")}?`,
28975
- initialValue: false
28976
- });
28977
- if (p29.isCancel(confirmed) || !confirmed) {
28978
- p29.cancel(`${removeIntegrationsMode ? "Integration" : "Preset"} removal cancelled.`);
28979
- process.exit(0);
28980
- }
28981
- }
28982
- if (removeIntegrationsMode) {
28983
- const result2 = await removeIntegrations({
28984
- cwd,
28985
- config,
28986
- pm,
28987
- integrationIds
28988
- });
28989
- writeConfigFile(cwd, result2.config);
28990
- if (result2.removed.length === 0) {
28991
- p29.outro("No integrations were removed.");
28992
- return;
28993
- }
28994
- if (result2.warnings.length > 0) {
28995
- p29.note(result2.warnings.join("\n"), "Warnings");
28996
- }
28997
- p29.outro(
28998
- `Removed integration${result2.removed.length === 1 ? "" : "s"}: ${result2.removed.join(", ")}`
28999
- );
29000
- return;
29001
- }
29002
- const result = await removePresets({
29003
- cwd,
29004
- config,
29005
- pm,
29006
- presetIds
29007
- });
29008
- writeConfigFile(cwd, result.config);
29009
- if (result.removed.length === 0) {
29010
- p29.outro("No presets were removed.");
29011
- return;
29012
- }
29013
- if (result.warnings.length > 0) {
29014
- p29.note(result.warnings.join("\n"), "Warnings");
29015
- }
29016
- p29.outro(`Removed preset${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`);
29017
- }
29018
-
29019
- // adapters/next/commands/remove-schema.ts
29020
- import fs42 from "fs";
29021
- import path57 from "path";
29022
- import * as clack3 from "@clack/prompts";
29023
- function removePath2(cwd, filePath) {
29024
- const fullPath = path57.join(cwd, ...filePath.split("/"));
29025
- const existed = fs42.existsSync(fullPath);
29026
- fs42.rmSync(fullPath, { recursive: true, force: true });
29027
- return existed;
29028
- }
29029
- function cleanupEmptyDirs3(cwd, deletedPaths, configPaths) {
29030
- const stopRoots = /* @__PURE__ */ new Set([
29031
- path57.join(cwd, ...configPaths.adminDir.split("/")),
29032
- path57.join(cwd, ...configPaths.adminNavigationDir.split("/")),
29033
- path57.join(cwd, ...configPaths.adminWebhookEventsDir.split("/")),
29034
- path57.join(cwd, ...configPaths.pagesDir.split("/"))
29035
- ]);
29036
- for (const deletedPath of deletedPaths) {
29037
- let current = path57.dirname(path57.join(cwd, ...deletedPath.split("/")));
29038
- while (!stopRoots.has(current)) {
29039
- if (!fs42.existsSync(current)) {
29040
- current = path57.dirname(current);
29041
- continue;
29042
- }
29043
- const entries = fs42.readdirSync(current);
29044
- if (entries.length > 0) {
29045
- break;
29046
- }
29047
- fs42.rmdirSync(current);
29048
- current = path57.dirname(current);
29049
- }
29050
- }
29051
- }
29052
- function resolveSchemaOwnerForRemoval(cwd, schemaName) {
29053
- const explicitOwner = getSchemaOwner(cwd, schemaName);
29054
- if (explicitOwner) {
29055
- return explicitOwner;
29056
- }
29057
- if (schemaName === "settings") {
29058
- return "core";
29059
- }
29060
- return "user";
29061
- }
29062
- async function runRemoveSchemaCommand(schemaName, options) {
29063
- const owner = resolveSchemaOwnerForRemoval(
29064
- options.cwd ? path57.resolve(options.cwd) : process.cwd(),
29065
- schemaName
29066
- );
29067
- if (owner === "core") {
29068
- clack3.log.error(
29069
- `"${schemaName}" is a core Admin module and cannot be removed with remove-schema.`
29070
- );
29071
- process.exit(1);
29072
- }
29073
- if (owner.startsWith("preset:")) {
29074
- clack3.log.error(`"${schemaName}" is owned by ${owner}. Remove the owning preset instead.`);
29075
- process.exit(1);
29076
- }
29077
- const cwd = options.cwd ? path57.resolve(options.cwd) : process.cwd();
29078
- const config = await resolveConfigOrExit(cwd);
29079
- const paths = resolveProjectPaths(config);
29080
- const manifest = loadManifest(cwd, schemaName);
29081
- if (!snapshotRootExists(cwd)) {
29082
- clack3.log.error(UNSUPPORTED_PROJECT_STATE_MESSAGE);
29083
- process.exit(1);
29084
- }
29085
- if (!manifest) {
29086
- clack3.log.error(UNSUPPORTED_PROJECT_STATE_MESSAGE);
29087
- process.exit(1);
29088
- }
29089
- if (!options.force) {
29090
- if (!isInteractiveSession()) {
29091
- clack3.log.error(
29092
- `Removing generated files for ${schemaName} needs confirmation. Re-run with --force.`
29093
- );
29094
- process.exit(1);
29095
- }
29096
- const confirmed = await clack3.confirm({
29097
- message: `Remove generated files for ${schemaName}?`,
29098
- initialValue: false
29099
- });
29100
- if (clack3.isCancel(confirmed) || !confirmed) {
29101
- clack3.cancel("Cancelled.");
29102
- return;
29103
- }
29104
- }
29105
- const deletedPaths = [];
29106
- for (const file of [...manifest.files.map((entry) => entry.path), ...manifest.skipped]) {
29107
- if (removePath2(cwd, file)) {
29108
- deletedPaths.push(file);
29109
- }
29110
- }
29111
- const loaded = (() => {
29112
- try {
29113
- return loadSchema(path57.join(cwd, ...paths.schemasDir.split("/")), schemaName);
29114
- } catch {
29115
- return null;
29116
- }
29117
- })();
29118
- const kebabName = toKebabCase(schemaName);
29119
- if (loaded?.type === "form") {
29120
- if (removePath2(cwd, `${paths.adminActionsDir}/${schemaName}`)) {
29121
- deletedPaths.push(`${paths.adminActionsDir}/${schemaName}`);
29122
- }
29123
- if (removePath2(cwd, `${paths.pagesDir}/forms/${kebabName}`)) {
29124
- deletedPaths.push(`${paths.pagesDir}/forms/${kebabName}`);
29125
- }
29126
- } else {
29127
- if (removePath2(cwd, `${paths.adminActionsDir}/${schemaName}`)) {
29128
- deletedPaths.push(`${paths.adminActionsDir}/${schemaName}`);
29129
- }
29130
- if (removePath2(cwd, `${paths.pagesDir}/${schemaName}`)) {
29131
- deletedPaths.push(`${paths.pagesDir}/${schemaName}`);
29132
- }
29133
- }
29134
- cleanupEmptyDirs3(cwd, deletedPaths, paths);
29135
- deleteSnapshot(cwd, schemaName);
29136
- if (hasTombstone(cwd, schemaName)) {
29137
- clearTombstone(cwd, schemaName);
29138
- }
29139
- writeTombstone(cwd, schemaName);
29140
- await applyGeneratedFiles({
29141
- cwd,
29142
- config,
29143
- scope: BARREL_SCOPE,
29144
- schemaJson: { name: BARREL_SCOPE },
29145
- generatedFiles: renderBarrelFiles(cwd, config),
29146
- force: false,
29147
- interactive: false
29148
- });
29149
- clack3.log.info(
29150
- `Tombstone written: .betterstart/snapshots/_removed/${schemaName}
29151
- Schema JSON preserved.`
29152
- );
29153
- clack3.outro(`Removed generated files for ${schemaName}`);
29154
- }
29155
-
29156
- // adapters/next/commands/uninstall.ts
29157
- import fs44 from "fs";
29158
- import path58 from "path";
29159
- import * as p30 from "@clack/prompts";
29160
- import pc11 from "picocolors";
29161
-
29162
- // adapters/next/commands/uninstall-cleaners.ts
29163
- import fs43 from "fs";
29164
- function stripJsonComments2(input) {
29165
- let result = "";
29166
- let i = 0;
29167
- while (i < input.length) {
29168
- if (input[i] === '"') {
29169
- let j = i + 1;
29170
- while (j < input.length) {
29171
- if (input[j] === "\\") {
29172
- j += 2;
29173
- continue;
29174
- }
29175
- if (input[j] === '"') {
29176
- j++;
29177
- break;
29178
- }
29179
- j++;
29180
- }
29181
- result += input.slice(i, j);
29182
- i = j;
29183
- } else if (input[i] === "/" && input[i + 1] === "/") {
29184
- const nl = input.indexOf("\n", i);
29185
- i = nl === -1 ? input.length : nl;
29186
- } else if (input[i] === "/" && input[i + 1] === "*") {
29187
- const end = input.indexOf("*/", i + 2);
29188
- i = end === -1 ? input.length : end + 2;
29189
- } else {
29190
- result += input[i];
29191
- i++;
29192
- }
29193
- }
29194
- return result;
29195
- }
29196
- function cleanTsconfig(tsconfigPath, aliasRoot = "@admin") {
29197
- if (!fs43.existsSync(tsconfigPath)) return [];
29198
- const raw = fs43.readFileSync(tsconfigPath, "utf-8");
29199
- const stripped = stripJsonComments2(raw).replace(/,\s*([\]}])/g, "$1");
29200
- let tsconfig;
29201
- try {
29202
- tsconfig = JSON.parse(stripped);
29203
- } catch {
29204
- return [];
29205
- }
29206
- const compilerOptions = tsconfig.compilerOptions ?? {};
29207
- const paths = compilerOptions.paths ?? {};
29208
- const removed = [];
29209
- for (const key of Object.keys(paths)) {
29210
- if (key.startsWith("@admin/") || key === "@admin/*" || key.startsWith(`${aliasRoot}/`) || key === `${aliasRoot}/*`) {
29211
- removed.push(key);
29212
- delete paths[key];
29213
- }
29214
- }
29215
- if (removed.length === 0) return [];
29216
- if (Object.keys(paths).length === 0) {
29217
- compilerOptions.paths = void 0;
29218
- } else {
29219
- compilerOptions.paths = paths;
29220
- }
29221
- tsconfig.compilerOptions = compilerOptions;
29222
- fs43.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
29223
- `, "utf-8");
29224
- return removed;
29225
- }
29226
- function cleanCss(cssPath, namespace = "admin") {
29227
- if (!fs43.existsSync(cssPath)) return [];
29228
- const content = fs43.readFileSync(cssPath, "utf-8");
29229
- const lines = content.split("\n");
29230
- const sourcePattern = new RegExp(`^@source\\s+"[^"]*(?:admin|${namespace})[^"]*";\\s*$`);
29231
- const removed = [];
29232
- const kept = [];
29233
- for (const line of lines) {
29234
- if (sourcePattern.test(line)) {
29235
- removed.push(line.trim());
29236
- } else {
29237
- kept.push(line);
29238
- }
29239
- }
29240
- if (removed.length === 0) return [];
29241
- const cleaned = kept.join("\n").replace(/\n{3,}/g, "\n\n");
29242
- fs43.writeFileSync(cssPath, cleaned, "utf-8");
29243
- return removed;
29244
- }
29245
- function cleanEnvFile(envPath) {
29246
- if (!fs43.existsSync(envPath)) return [];
29247
- const content = fs43.readFileSync(envPath, "utf-8");
29248
- const lines = content.split("\n");
29249
- const removed = [];
29250
- const kept = [];
29251
- const headerPattern = /^# =+$/;
29252
- const headerTextPattern = /^# BetterStart Admin$/;
29253
- for (let i = 0; i < lines.length; i++) {
29254
- const line = lines[i];
29255
- const trimmed = line.trim();
29256
- if (trimmed.match(/^BETTERSTART_\w+=/)) {
29257
- const key = trimmed.split("=")[0];
29258
- removed.push(key);
29259
- continue;
29260
- }
29261
- if (headerPattern.test(trimmed)) {
29262
- const next = lines[i + 1]?.trim();
29263
- const afterNext = lines[i + 2]?.trim();
29264
- if (next && headerTextPattern.test(next) && afterNext && headerPattern.test(afterNext)) {
29265
- i += 2;
29266
- continue;
29267
- }
29268
- }
29269
- if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
29270
- const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
29271
- if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
29272
- continue;
29273
- }
29274
- }
29275
- kept.push(line);
29276
- }
29277
- if (removed.length === 0) return [];
29278
- const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
29279
- if (result === "") {
29280
- fs43.unlinkSync(envPath);
29281
- } else {
29282
- fs43.writeFileSync(envPath, `${result}
29283
- `, "utf-8");
29284
- }
29285
- return removed;
29286
- }
29287
- function findNextNonEmptyLine(lines, startIndex) {
29288
- for (let i = startIndex; i < lines.length; i++) {
29289
- const trimmed = lines[i].trim();
29290
- if (trimmed !== "") return trimmed;
29291
- }
29292
- return null;
29293
- }
29294
-
29295
- // adapters/next/commands/uninstall.ts
29296
- function findMainCss2(cwd) {
29297
- const candidates = [
29298
- "src/app/globals.css",
29299
- "app/globals.css",
29300
- "src/app/global.css",
29301
- "app/global.css",
29302
- "src/app/app.css",
29303
- "app/app.css",
29304
- "src/globals.css",
29305
- "globals.css"
29306
- ];
29307
- for (const candidate of candidates) {
29308
- const filePath = path58.join(cwd, candidate);
29309
- if (fs44.existsSync(filePath)) return filePath;
29310
- }
29311
- return void 0;
29312
- }
29313
- function isCLICreatedBiome(biomePath) {
29314
- if (!fs44.existsSync(biomePath)) return false;
29315
- try {
29316
- const content = JSON.parse(fs44.readFileSync(biomePath, "utf-8"));
29317
- return content.$schema?.includes("biomejs.dev") && content.formatter?.indentStyle === "space" && content.javascript?.formatter?.quoteStyle === "single" && Array.isArray(content.files?.ignore) && content.files.ignore.includes(".next");
29318
- } catch {
29319
- return false;
29320
- }
29321
- }
29322
- function buildUninstallPlan(cwd, namespaceValue) {
29323
- const steps = [];
29324
- const namespace = resolveAdminNamespace(namespaceValue);
29325
- const hasSrc = fs44.existsSync(path58.join(cwd, "src"));
29326
- const appBase = hasSrc ? "src/app" : "app";
29327
- const dirs = [];
29328
- const adminDir = path58.join(cwd, namespace.segment);
29329
- const legacyAdminDir = path58.join(cwd, "admin");
29330
- const adminRouteGroup = path58.join(cwd, appBase, namespace.routeGroup);
29331
- const legacyAdminRouteGroup = path58.join(cwd, appBase, "(admin)");
29332
- if (fs44.existsSync(adminDir)) dirs.push(`${namespace.segment}/`);
29333
- if (namespace.segment !== "admin" && fs44.existsSync(legacyAdminDir)) dirs.push("admin/");
29334
- if (fs44.existsSync(adminRouteGroup)) dirs.push(`${appBase}/${namespace.routeGroup}/`);
29335
- if (namespace.segment !== "admin" && fs44.existsSync(legacyAdminRouteGroup))
29336
- dirs.push(`${appBase}/(admin)/`);
29337
- if (dirs.length > 0) {
29338
- steps.push({
29339
- label: "Admin directories",
29340
- items: dirs,
29341
- count: dirs.length,
29342
- unit: dirs.length === 1 ? "directory" : "directories",
29343
- execute() {
29344
- if (fs44.existsSync(adminDir)) fs44.rmSync(adminDir, { recursive: true, force: true });
29345
- if (fs44.existsSync(legacyAdminDir))
29346
- fs44.rmSync(legacyAdminDir, { recursive: true, force: true });
29347
- if (fs44.existsSync(adminRouteGroup)) {
29348
- fs44.rmSync(adminRouteGroup, { recursive: true, force: true });
29349
- }
29350
- if (fs44.existsSync(legacyAdminRouteGroup)) {
29351
- fs44.rmSync(legacyAdminRouteGroup, { recursive: true, force: true });
29352
- }
29353
- }
29354
- });
29355
- }
29356
- const configFiles = [];
29357
- const configPaths = [];
29358
- const candidates = [
29359
- [CONFIG_FILE_NAME, path58.join(cwd, CONFIG_FILE_NAME)],
29360
- ["drizzle.config.ts", path58.join(cwd, "drizzle.config.ts")],
29361
- ["ADMIN.md", path58.join(cwd, "ADMIN.md")]
29362
- ];
29363
- for (const [label, fullPath] of candidates) {
29364
- if (fs44.existsSync(fullPath)) {
29365
- configFiles.push(label);
29366
- configPaths.push(fullPath);
29367
- }
29368
- }
29369
- const biomePath = path58.join(cwd, "biome.json");
29370
- if (isCLICreatedBiome(biomePath)) {
29371
- configFiles.push("biome.json (CLI-created)");
29372
- configPaths.push(biomePath);
29373
- }
29374
- if (configFiles.length > 0) {
29375
- steps.push({
29376
- label: "Config files",
29377
- items: configFiles,
29378
- count: configFiles.length,
29379
- unit: configFiles.length === 1 ? "file" : "files",
29380
- execute() {
29381
- for (const p32 of configPaths) {
29382
- if (fs44.existsSync(p32)) fs44.unlinkSync(p32);
29383
- }
29384
- }
29385
- });
29386
- }
29387
- const tsconfigPath = path58.join(cwd, "tsconfig.json");
29388
- if (fs44.existsSync(tsconfigPath)) {
29389
- const content = fs44.readFileSync(tsconfigPath, "utf-8");
29390
- const aliasMatches = [
29391
- ...content.match(/"@admin\//g) ?? [],
29392
- ...content.match(new RegExp(`"${namespace.alias}/`, "g")) ?? []
29393
- ];
29394
- if (aliasMatches && aliasMatches.length > 0) {
29395
- const aliasCount = aliasMatches.length;
29396
- steps.push({
29397
- label: "tsconfig.json path aliases",
29398
- items: [`${namespace.alias}/* aliases in tsconfig.json`],
29399
- count: aliasCount,
29400
- unit: aliasCount === 1 ? "alias" : "aliases",
29401
- execute() {
29402
- cleanTsconfig(tsconfigPath, namespace.alias);
29403
- }
29404
- });
29405
- }
29406
- }
29407
- const cssFile = findMainCss2(cwd);
29408
- if (cssFile) {
29409
- const cssContent = fs44.readFileSync(cssFile, "utf-8");
29410
- const sourceLines = cssContent.split("\n").filter(
29411
- (l) => new RegExp(`^@source\\s+"[^"]*(?:admin|${namespace.segment})[^"]*";\\s*$`).test(l)
29412
- );
29413
- if (sourceLines.length > 0) {
29414
- const relCss = path58.relative(cwd, cssFile);
29415
- steps.push({
29416
- label: `CSS @source lines (${relCss})`,
29417
- items: [`@source lines in ${relCss}`],
29418
- count: sourceLines.length,
29419
- unit: sourceLines.length === 1 ? "line" : "lines",
29420
- execute() {
29421
- cleanCss(cssFile, namespace.segment);
29422
- }
29423
- });
29424
- }
29425
- }
29426
- const envPath = path58.join(cwd, ".env.local");
29427
- if (fs44.existsSync(envPath)) {
29428
- const envContent = fs44.readFileSync(envPath, "utf-8");
29429
- const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
29430
- if (bsVars.length > 0) {
29431
- steps.push({
29432
- label: ".env.local variables",
29433
- items: ["BETTERSTART_* vars in .env.local"],
29434
- count: bsVars.length,
29435
- unit: bsVars.length === 1 ? "variable" : "variables",
29436
- execute() {
29437
- cleanEnvFile(envPath);
29438
- }
29439
- });
29440
- }
29441
- }
29442
- return steps;
29443
- }
29444
- async function runUninstallCommand(options) {
29445
- const cwd = options.cwd ? path58.resolve(options.cwd) : process.cwd();
29446
- p30.intro(pc11.bgRed(pc11.white(" BetterStart Uninstall ")));
29447
- let namespace = DEFAULT_ADMIN_NAMESPACE;
29448
- try {
29449
- const config = await resolveConfig(cwd);
29450
- namespace = config.frameworkConfig.next.namespace;
29451
- } catch {
29452
- }
29453
- const steps = buildUninstallPlan(cwd, namespace);
29454
- if (steps.length === 0) {
29455
- p30.log.success(`${pc11.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
29456
- p30.outro("Project already clean");
29457
- return;
29458
- }
29459
- const planLines = steps.map((step) => {
29460
- const names = step.items.join(" ");
29461
- const countLabel = pc11.dim(`${step.count} ${step.unit}`);
29462
- return `${pc11.red("\u2717")} ${names} ${countLabel}`;
29463
- });
29464
- p30.note(planLines.join("\n"), "Uninstall plan");
29465
- if (!options.force) {
29466
- const confirmed = await p30.confirm({
29467
- message: "Proceed with uninstall?",
29468
- initialValue: false
29469
- });
29470
- if (p30.isCancel(confirmed) || !confirmed) {
29471
- p30.cancel("Uninstall cancelled.");
29472
- process.exit(0);
29473
- }
29474
- }
29475
- const s = spinner2();
29476
- s.start(steps[0].label);
29477
- for (const step of steps) {
29478
- s.message(step.label);
29479
- step.execute();
29480
- }
29481
- const parts = steps.map((step) => `${step.count} ${step.unit}`);
29482
- s.stop(`Removed ${parts.join(", ")}`);
29483
- p30.note(pc11.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
29484
- p30.outro("Uninstall complete");
29485
- }
29486
29107
 
29487
29108
  // adapters/next/commands/update-component.ts
29488
29109
  import { execFileSync as execFileSync5 } from "child_process";
29489
- import fs45 from "fs";
29490
- import path59 from "path";
29491
- import * as clack4 from "@clack/prompts";
29110
+ import fs42 from "fs";
29111
+ import path55 from "path";
29112
+ import * as clack3 from "@clack/prompts";
29492
29113
  import fsExtra from "fs-extra";
29493
29114
  var STATIC_CUSTOM_DEPENDENCIES = {
29494
29115
  "content-editor": [
@@ -29738,24 +29359,24 @@ function applyNamespaceToTemplateEntry(entry, config, cwd) {
29738
29359
  };
29739
29360
  }
29740
29361
  function writeNamespacedFile(srcPath, destPath, namespace) {
29741
- fs45.writeFileSync(
29362
+ fs42.writeFileSync(
29742
29363
  destPath,
29743
- applyAdminNamespaceToContent(fs45.readFileSync(srcPath, "utf-8"), namespace),
29364
+ applyAdminNamespaceToContent(fs42.readFileSync(srcPath, "utf-8"), namespace),
29744
29365
  "utf-8"
29745
29366
  );
29746
29367
  }
29747
29368
  function copyNamespacedDirectory(srcDir, destDir, namespace) {
29748
- const entries = fs45.readdirSync(srcDir, { withFileTypes: true });
29369
+ const entries = fs42.readdirSync(srcDir, { withFileTypes: true });
29749
29370
  for (const entry of entries) {
29750
29371
  const namespacedName = applyAdminNamespaceToPath(entry.name, namespace);
29751
- const srcPath = path59.join(srcDir, entry.name);
29752
- const destPath = path59.join(destDir, namespacedName);
29372
+ const srcPath = path55.join(srcDir, entry.name);
29373
+ const destPath = path55.join(destDir, namespacedName);
29753
29374
  if (entry.isDirectory()) {
29754
29375
  fsExtra.ensureDirSync(destPath);
29755
29376
  copyNamespacedDirectory(srcPath, destPath, namespace);
29756
29377
  continue;
29757
29378
  }
29758
- fsExtra.ensureDirSync(path59.dirname(destPath));
29379
+ fsExtra.ensureDirSync(path55.dirname(destPath));
29759
29380
  writeNamespacedFile(srcPath, destPath, namespace);
29760
29381
  }
29761
29382
  }
@@ -29766,10 +29387,10 @@ function hasIntegration(config, integrationId) {
29766
29387
  return config.integrations.installed.includes(integrationId);
29767
29388
  }
29768
29389
  function readProjectPackageJson2(cwd) {
29769
- const pkgPath = path59.join(cwd, "package.json");
29770
- if (!fs45.existsSync(pkgPath)) return null;
29390
+ const pkgPath = path55.join(cwd, "package.json");
29391
+ if (!fs42.existsSync(pkgPath)) return null;
29771
29392
  try {
29772
- return JSON.parse(fs45.readFileSync(pkgPath, "utf-8"));
29393
+ return JSON.parse(fs42.readFileSync(pkgPath, "utf-8"));
29773
29394
  } catch {
29774
29395
  return null;
29775
29396
  }
@@ -30653,13 +30274,38 @@ var TEMPLATE_REGISTRY = {
30653
30274
  relPath: "app/(admin)/admin/(authenticated)/settings/forms/forms-settings-page-content.tsx",
30654
30275
  content: () => readTemplate("pages/settings/forms/forms-settings-page-content.tsx"),
30655
30276
  base: "cwd",
30656
- dependencies: ["edit-form-notifications-dialog", "use-webhooks"]
30277
+ dependencies: [
30278
+ "form-notifications-drawer",
30279
+ "forms-settings-columns",
30280
+ "forms-settings-table",
30281
+ "page-header",
30282
+ "use-webhooks"
30283
+ ]
30284
+ },
30285
+ "forms-settings-columns": {
30286
+ relPath: "app/(admin)/admin/(authenticated)/settings/forms/forms-settings-columns.tsx",
30287
+ content: () => readTemplate("pages/settings/forms/forms-settings-columns.tsx"),
30288
+ base: "cwd"
30289
+ },
30290
+ "forms-settings-table": {
30291
+ relPath: "app/(admin)/admin/(authenticated)/settings/forms/forms-settings-table.tsx",
30292
+ content: () => readTemplate("pages/settings/forms/forms-settings-table.tsx"),
30293
+ base: "cwd",
30294
+ dependencies: ["data-grid"]
30657
30295
  },
30658
- "edit-form-notifications-dialog": {
30659
- relPath: "app/(admin)/admin/(authenticated)/settings/forms/edit-form-notifications-dialog.tsx",
30660
- content: () => readTemplate("pages/settings/forms/edit-form-notifications-dialog.tsx"),
30296
+ "form-notifications-drawer": {
30297
+ relPath: "app/(admin)/admin/(authenticated)/settings/forms/form-notifications-drawer.tsx",
30298
+ content: () => readTemplate("pages/settings/forms/form-notifications-drawer.tsx"),
30661
30299
  base: "cwd",
30662
- dependencies: ["form-settings-action"]
30300
+ dependencies: [
30301
+ "button",
30302
+ "card",
30303
+ "drawer",
30304
+ "form",
30305
+ "form-settings-action",
30306
+ "scroll-area",
30307
+ "textarea"
30308
+ ]
30663
30309
  },
30664
30310
  "webhooks-page": {
30665
30311
  relPath: "app/(admin)/admin/(authenticated)/settings/webhooks/page.tsx",
@@ -31277,577 +30923,1151 @@ function getStaticUiComponents() {
31277
30923
  function getStaticUiComponentEntries() {
31278
30924
  return getStaticAssetComponentEntries("ui");
31279
30925
  }
31280
- function getStaticCustomComponents() {
31281
- return getStaticAssetComponents("custom");
30926
+ function getStaticCustomComponents() {
30927
+ return getStaticAssetComponents("custom");
30928
+ }
30929
+ function getStaticCustomComponentEntries() {
30930
+ return getStaticAssetComponentEntries("custom");
30931
+ }
30932
+ function getStaticAssetComponents(assetDirectory) {
30933
+ return getStaticAssetComponentEntries(assetDirectory).map((entry) => entry.name);
30934
+ }
30935
+ function getStaticAssetComponentEntries(assetDirectory) {
30936
+ const assetDir = resolveCliAssetPath("shared-assets", "react-admin", assetDirectory);
30937
+ if (!fs42.existsSync(assetDir)) return [];
30938
+ const components = [];
30939
+ for (const entry of fs42.readdirSync(assetDir, { withFileTypes: true })) {
30940
+ if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
30941
+ components.push({
30942
+ name: entry.name.replace(/\.(tsx|ts)$/, ""),
30943
+ file: entry.name
30944
+ });
30945
+ continue;
30946
+ }
30947
+ if (!entry.isDirectory()) {
30948
+ continue;
30949
+ }
30950
+ const indexFile = ["index.tsx", "index.ts"].find(
30951
+ (file) => fs42.existsSync(path55.join(assetDir, entry.name, file))
30952
+ );
30953
+ if (indexFile) {
30954
+ components.push({
30955
+ name: entry.name,
30956
+ file: path55.join(entry.name, indexFile)
30957
+ });
30958
+ }
30959
+ }
30960
+ return components.sort((a, b) => a.name.localeCompare(b.name));
30961
+ }
30962
+ function findStaticAssetFile(assetDir, componentName) {
30963
+ if (!fs42.existsSync(assetDir)) return void 0;
30964
+ const isNestedComponentName = /[\\/]/.test(componentName);
30965
+ const nestedComponentName = componentName.split(/[\\/]/).filter(Boolean).join(path55.sep);
30966
+ if (isNestedComponentName && nestedComponentName && !path55.isAbsolute(componentName) && !nestedComponentName.split(path55.sep).includes("..")) {
30967
+ for (const extension of [".tsx", ".ts"]) {
30968
+ const relPath = `${nestedComponentName}${extension}`;
30969
+ const filePath = path55.join(assetDir, relPath);
30970
+ if (fs42.existsSync(filePath) && fs42.statSync(filePath).isFile()) {
30971
+ return relPath;
30972
+ }
30973
+ }
30974
+ }
30975
+ if (!isNestedComponentName && !componentName.includes("..") && !path55.isAbsolute(componentName)) {
30976
+ for (const extension of [".tsx", ".ts"]) {
30977
+ const relPath = path55.join(componentName, `index${extension}`);
30978
+ const filePath = path55.join(assetDir, relPath);
30979
+ if (fs42.existsSync(filePath) && fs42.statSync(filePath).isFile()) {
30980
+ return relPath;
30981
+ }
30982
+ }
30983
+ }
30984
+ return fs42.readdirSync(assetDir, { withFileTypes: true }).find(
30985
+ (entry) => entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) && entry.name.replace(/\.(tsx|ts)$/, "") === componentName
30986
+ )?.name;
31282
30987
  }
31283
- function getStaticCustomComponentEntries() {
31284
- return getStaticAssetComponentEntries("custom");
30988
+ function getAllComponentNames() {
30989
+ const staticUi = getStaticUiComponents();
30990
+ const staticCustom = getStaticCustomComponents();
30991
+ const templateKeys = Object.keys(TEMPLATE_REGISTRY);
30992
+ return [.../* @__PURE__ */ new Set([...staticUi, ...staticCustom, ...templateKeys, "tiptap"])].sort();
31285
30993
  }
31286
- function getStaticAssetComponents(assetDirectory) {
31287
- return getStaticAssetComponentEntries(assetDirectory).map((entry) => entry.name);
30994
+ function getAllComponentNamesForConfig(config) {
30995
+ const staticUi = getStaticUiComponents();
30996
+ const staticCustom = getStaticCustomComponents();
30997
+ const templateKeys = Object.entries(TEMPLATE_REGISTRY).filter(
30998
+ ([, entry]) => (!entry.requiredPreset || hasPreset(config, entry.requiredPreset)) && (!entry.requiredIntegration || hasIntegration(config, entry.requiredIntegration))
30999
+ ).map(([name]) => name);
31000
+ return [.../* @__PURE__ */ new Set([...staticUi, ...staticCustom, ...templateKeys, "tiptap"])].sort();
31288
31001
  }
31289
- function getStaticAssetComponentEntries(assetDirectory) {
31290
- const assetDir = resolveCliAssetPath("shared-assets", "react-admin", assetDirectory);
31291
- if (!fs45.existsSync(assetDir)) return [];
31292
- const components = [];
31293
- for (const entry of fs45.readdirSync(assetDir, { withFileTypes: true })) {
31294
- if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
31295
- components.push({
31296
- name: entry.name.replace(/\.(tsx|ts)$/, ""),
31297
- file: entry.name
31002
+ async function runUpdateCommand(components, options) {
31003
+ const cwd = options.cwd ? path55.resolve(options.cwd) : process.cwd();
31004
+ const normalizedOnly = normalizeShadcnPresetOnly(options.only);
31005
+ validateShadcnPresetOptions(components, options);
31006
+ if (options.json && !options.list) {
31007
+ clack3.cancel("--json can only be used with --list.");
31008
+ process.exit(1);
31009
+ }
31010
+ if (options.list) {
31011
+ const uiComponents = getStaticUiComponentEntries();
31012
+ const customComponents = getStaticCustomComponentEntries();
31013
+ const templateKeys = Object.keys(TEMPLATE_REGISTRY).sort();
31014
+ const templatePath = (name) => {
31015
+ const entry = TEMPLATE_REGISTRY[name];
31016
+ return entry.displayPath ?? (typeof entry.relPath === "string" ? entry.relPath : "");
31017
+ };
31018
+ if (options.json) {
31019
+ const items = [
31020
+ ...uiComponents.map((component) => ({
31021
+ name: component.name,
31022
+ path: `components/ui/${component.file}`,
31023
+ kind: "ui"
31024
+ })),
31025
+ ...customComponents.map((component) => ({
31026
+ name: component.name,
31027
+ path: `components/custom/${component.file}`,
31028
+ kind: "custom"
31029
+ })),
31030
+ ...templateKeys.map((name) => ({ name, path: templatePath(name), kind: "template" })),
31031
+ { name: "tiptap", path: "components/custom/content-editor/tiptap-*/", kind: "special" }
31032
+ ];
31033
+ console.log(JSON.stringify(items, null, 2));
31034
+ return;
31035
+ }
31036
+ const all = getAllComponentNames();
31037
+ clack3.intro("Available components");
31038
+ clack3.note(
31039
+ renderTableRows(
31040
+ uiComponents.map((component) => [component.name, `components/ui/${component.file}`])
31041
+ ).join("\n"),
31042
+ `Shadcn UI Components (${uiComponents.length})`
31043
+ );
31044
+ clack3.note(
31045
+ renderTableRows(
31046
+ customComponents.map((component) => [component.name, `components/custom/${component.file}`])
31047
+ ).join("\n"),
31048
+ `Custom Components (${customComponents.length})`
31049
+ );
31050
+ clack3.note(
31051
+ renderTableRows(templateKeys.map((name) => [name, templatePath(name)])).join("\n"),
31052
+ `Template Components (${templateKeys.length})`
31053
+ );
31054
+ clack3.note(
31055
+ renderTableRows([["tiptap", "components/custom/content-editor/tiptap-*/ (all files)"]]).join(
31056
+ "\n"
31057
+ ),
31058
+ "Special"
31059
+ );
31060
+ clack3.outro(`${all.length} components available`);
31061
+ return;
31062
+ }
31063
+ const config = await resolveConfigOrExit(cwd);
31064
+ const admin = path55.resolve(cwd, config.paths.admin);
31065
+ if (!fs42.existsSync(admin)) {
31066
+ clack3.cancel(
31067
+ `Admin directory not found at ${config.paths.admin}. Run 'betterstart init' first.`
31068
+ );
31069
+ process.exit(1);
31070
+ }
31071
+ if (options.shadcnPreset) {
31072
+ runShadcnPresetUpdate({
31073
+ cwd,
31074
+ config,
31075
+ preset: options.shadcnPreset.trim(),
31076
+ only: normalizedOnly
31077
+ });
31078
+ return;
31079
+ }
31080
+ if (!options.all && components.length === 0) {
31081
+ clack3.log.error(
31082
+ "Provide component names or use --all. Run with --list to see available components."
31083
+ );
31084
+ process.exit(1);
31085
+ }
31086
+ clack3.intro("BetterStart Update Components");
31087
+ const toUpdate = options.all ? getAllComponentNamesForConfig(config) : components;
31088
+ const uiDir = resolveCliAssetPath("shared-assets", "react-admin", "ui");
31089
+ const customDir = resolveCliAssetPath("shared-assets", "react-admin", "custom");
31090
+ let updated = 0;
31091
+ let skipped = 0;
31092
+ const updatedTemplateNames = /* @__PURE__ */ new Set();
31093
+ const updatedStaticNames = /* @__PURE__ */ new Set();
31094
+ const requiredPackageDependencies = /* @__PURE__ */ new Set();
31095
+ const pendingWrites = [];
31096
+ function trackPackageDependencies(name) {
31097
+ for (const dependency of COMPONENT_PACKAGE_DEPENDENCIES[name] ?? []) {
31098
+ requiredPackageDependencies.add(dependency);
31099
+ }
31100
+ }
31101
+ function writeTemplateEntry(name, entry) {
31102
+ if (updatedTemplateNames.has(name)) {
31103
+ return false;
31104
+ }
31105
+ if (entry.requiredPreset && !hasPreset(config, entry.requiredPreset)) {
31106
+ clack3.log.warn(`${name} requires the ${entry.requiredPreset} preset and was skipped.`);
31107
+ skipped++;
31108
+ return false;
31109
+ }
31110
+ if (entry.requiredIntegration && !hasIntegration(config, entry.requiredIntegration)) {
31111
+ clack3.log.warn(
31112
+ `${name} requires the ${entry.requiredIntegration} integration and was skipped.`
31113
+ );
31114
+ skipped++;
31115
+ return false;
31116
+ }
31117
+ const { relPath, content } = applyNamespaceToTemplateEntry(entry, config, cwd);
31118
+ const baseDir = entry.base === "cwd" ? cwd : admin;
31119
+ const destPath = path55.join(baseDir, relPath);
31120
+ if (entry.preserveExisting && fs42.existsSync(destPath)) {
31121
+ clack3.log.info(`Preserved ${relPath}`);
31122
+ updatedTemplateNames.add(name);
31123
+ skipped++;
31124
+ return false;
31125
+ }
31126
+ pendingWrites.push({
31127
+ displayPath: relPath,
31128
+ write: () => {
31129
+ fsExtra.ensureDirSync(path55.dirname(destPath));
31130
+ fs42.writeFileSync(destPath, content, "utf-8");
31131
+ clack3.log.success(`Updated ${relPath}`);
31132
+ }
31133
+ });
31134
+ updatedTemplateNames.add(name);
31135
+ trackPackageDependencies(name);
31136
+ updated++;
31137
+ return true;
31138
+ }
31139
+ function writeTemplateEntryWithDependencies(name, entry) {
31140
+ const wroteEntry = writeTemplateEntry(name, entry);
31141
+ if (!wroteEntry) {
31142
+ return false;
31143
+ }
31144
+ for (const dependencyName of entry.dependencies ?? []) {
31145
+ writeNamedDependency(dependencyName);
31146
+ }
31147
+ return true;
31148
+ }
31149
+ function writeStaticAssetEntry(assetDirectory, name) {
31150
+ const key = `${assetDirectory}:${name}`;
31151
+ if (updatedStaticNames.has(key)) {
31152
+ return false;
31153
+ }
31154
+ const assetDir = assetDirectory === "ui" ? uiDir : customDir;
31155
+ const assetFile = findStaticAssetFile(assetDir, name);
31156
+ if (!assetFile) {
31157
+ return false;
31158
+ }
31159
+ const namespace = config.frameworkConfig.next.namespace;
31160
+ const namespacedAssetFile = applyAdminNamespaceToPath(assetFile, namespace);
31161
+ const destPath = path55.join(admin, "components", assetDirectory, namespacedAssetFile);
31162
+ pendingWrites.push({
31163
+ displayPath: `components/${assetDirectory}/${namespacedAssetFile}`,
31164
+ write: () => {
31165
+ fsExtra.ensureDirSync(path55.dirname(destPath));
31166
+ writeNamespacedFile(path55.join(assetDir, assetFile), destPath, namespace);
31167
+ clack3.log.success(`Updated components/${assetDirectory}/${namespacedAssetFile}`);
31168
+ }
31169
+ });
31170
+ if (assetDirectory === "custom") {
31171
+ const assetSubdir = path55.join(assetDir, name);
31172
+ if (fs42.existsSync(assetSubdir) && fs42.statSync(assetSubdir).isDirectory()) {
31173
+ const namespacedName = applyAdminNamespaceToPath(name, namespace);
31174
+ const destSubdir = path55.join(admin, "components", assetDirectory, namespacedName);
31175
+ pendingWrites.push({
31176
+ displayPath: `components/${assetDirectory}/${namespacedName}/ (template files)`,
31177
+ write: () => {
31178
+ fsExtra.ensureDirSync(destSubdir);
31179
+ copyNamespacedDirectory(assetSubdir, destSubdir, namespace);
31180
+ clack3.log.success(
31181
+ `Updated components/${assetDirectory}/${namespacedName}/ (template files)`
31182
+ );
31183
+ }
31184
+ });
31185
+ }
31186
+ }
31187
+ updatedStaticNames.add(key);
31188
+ trackPackageDependencies(name);
31189
+ updated++;
31190
+ if (assetDirectory === "custom") {
31191
+ for (const dependencyName of STATIC_CUSTOM_DEPENDENCIES[name] ?? []) {
31192
+ writeNamedDependency(dependencyName);
31193
+ }
31194
+ }
31195
+ return true;
31196
+ }
31197
+ function writeTiptapTemplates() {
31198
+ const key = "custom:tiptap";
31199
+ if (updatedStaticNames.has(key)) {
31200
+ return true;
31201
+ }
31202
+ const srcBaseDir = resolveCliAssetPath(
31203
+ "shared-assets",
31204
+ "react-admin",
31205
+ "custom",
31206
+ "content-editor"
31207
+ );
31208
+ const namespace = config.frameworkConfig.next.namespace;
31209
+ const destBaseDir = path55.join(admin, "components", "custom", "content-editor");
31210
+ if (!fs42.existsSync(srcBaseDir)) {
31211
+ return false;
31212
+ }
31213
+ const dirsToCopy = [];
31214
+ for (const directory of TIPTAP_CONTENT_EDITOR_DIRECTORIES) {
31215
+ const srcDir = path55.join(srcBaseDir, directory);
31216
+ if (!fs42.existsSync(srcDir)) {
31217
+ continue;
31218
+ }
31219
+ dirsToCopy.push({
31220
+ srcDir,
31221
+ destDir: path55.join(destBaseDir, applyAdminNamespaceToPath(directory, namespace))
31298
31222
  });
31223
+ }
31224
+ if (dirsToCopy.length === 0) {
31225
+ return false;
31226
+ }
31227
+ pendingWrites.push({
31228
+ displayPath: "components/custom/content-editor/tiptap-*/ (template files)",
31229
+ write: () => {
31230
+ for (const { srcDir, destDir } of dirsToCopy) {
31231
+ fsExtra.ensureDirSync(destDir);
31232
+ copyNamespacedDirectory(srcDir, destDir, namespace);
31233
+ }
31234
+ clack3.log.success("Updated components/custom/content-editor/tiptap-*/ (template files)");
31235
+ removeLegacyDirectory(path55.join(admin, "components", "custom", "tiptap"));
31236
+ }
31237
+ });
31238
+ updatedStaticNames.add(key);
31239
+ trackPackageDependencies("tiptap");
31240
+ updated++;
31241
+ for (const dependencyName of TIPTAP_TEMPLATE_DEPENDENCIES) {
31242
+ writeNamedDependency(dependencyName);
31243
+ }
31244
+ return true;
31245
+ }
31246
+ function writeNamedDependency(name) {
31247
+ const entry = TEMPLATE_REGISTRY[name];
31248
+ if (entry) {
31249
+ writeTemplateEntryWithDependencies(name, entry);
31250
+ return;
31251
+ }
31252
+ if (writeStaticAssetEntry("ui", name)) {
31253
+ return;
31254
+ }
31255
+ if (writeStaticAssetEntry("custom", name)) {
31256
+ return;
31257
+ }
31258
+ if (name === "tiptap") {
31259
+ writeTiptapTemplates();
31260
+ }
31261
+ }
31262
+ for (const name of toUpdate) {
31263
+ if (TEMPLATE_REGISTRY[name]) {
31264
+ const entry = TEMPLATE_REGISTRY[name];
31265
+ writeTemplateEntryWithDependencies(name, entry);
31299
31266
  continue;
31300
31267
  }
31301
- if (!entry.isDirectory()) {
31268
+ if (writeStaticAssetEntry("ui", name)) {
31302
31269
  continue;
31303
31270
  }
31304
- const indexFile = ["index.tsx", "index.ts"].find(
31305
- (file) => fs45.existsSync(path59.join(assetDir, entry.name, file))
31306
- );
31307
- if (indexFile) {
31308
- components.push({
31309
- name: entry.name,
31310
- file: path59.join(entry.name, indexFile)
31311
- });
31271
+ if (writeStaticAssetEntry("custom", name)) {
31272
+ continue;
31312
31273
  }
31313
- }
31314
- return components.sort((a, b) => a.name.localeCompare(b.name));
31315
- }
31316
- function findStaticAssetFile(assetDir, componentName) {
31317
- if (!fs45.existsSync(assetDir)) return void 0;
31318
- const isNestedComponentName = /[\\/]/.test(componentName);
31319
- const nestedComponentName = componentName.split(/[\\/]/).filter(Boolean).join(path59.sep);
31320
- if (isNestedComponentName && nestedComponentName && !path59.isAbsolute(componentName) && !nestedComponentName.split(path59.sep).includes("..")) {
31321
- for (const extension of [".tsx", ".ts"]) {
31322
- const relPath = `${nestedComponentName}${extension}`;
31323
- const filePath = path59.join(assetDir, relPath);
31324
- if (fs45.existsSync(filePath) && fs45.statSync(filePath).isFile()) {
31325
- return relPath;
31274
+ if (name === "tiptap") {
31275
+ if (!writeTiptapTemplates()) {
31276
+ clack3.log.warn("tiptap templates not found");
31277
+ skipped++;
31326
31278
  }
31279
+ continue;
31327
31280
  }
31281
+ clack3.log.warn(`Unknown component: ${name}`);
31282
+ skipped++;
31328
31283
  }
31329
- if (!isNestedComponentName && !componentName.includes("..") && !path59.isAbsolute(componentName)) {
31330
- for (const extension of [".tsx", ".ts"]) {
31331
- const relPath = path59.join(componentName, `index${extension}`);
31332
- const filePath = path59.join(assetDir, relPath);
31333
- if (fs45.existsSync(filePath) && fs45.statSync(filePath).isFile()) {
31334
- return relPath;
31284
+ if (pendingWrites.length > 0) {
31285
+ const displayPaths = pendingWrites.map((entry) => entry.displayPath);
31286
+ const preview = displayPaths.slice(0, MAX_OVERWRITE_PREVIEW_ROWS);
31287
+ if (displayPaths.length > preview.length) {
31288
+ preview.push(`\u2026 and ${displayPaths.length - preview.length} more`);
31289
+ }
31290
+ clack3.note(
31291
+ preview.join("\n"),
31292
+ `Overwriting ${displayPaths.length} file${displayPaths.length === 1 ? "" : "s"}`
31293
+ );
31294
+ if (!options.yes) {
31295
+ if (!isInteractiveSession()) {
31296
+ clack3.log.error(
31297
+ `Overwriting ${displayPaths.length} file${displayPaths.length === 1 ? "" : "s"} needs confirmation. Re-run with --yes.`
31298
+ );
31299
+ process.exit(1);
31300
+ }
31301
+ const proceed = await clack3.confirm({
31302
+ message: "Overwrite these files with the latest templates?",
31303
+ initialValue: true
31304
+ });
31305
+ if (clack3.isCancel(proceed) || !proceed) {
31306
+ clack3.cancel("Update cancelled.");
31307
+ process.exit(0);
31335
31308
  }
31336
31309
  }
31310
+ for (const entry of pendingWrites) {
31311
+ entry.write();
31312
+ }
31337
31313
  }
31338
- return fs45.readdirSync(assetDir, { withFileTypes: true }).find(
31339
- (entry) => entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) && entry.name.replace(/\.(tsx|ts)$/, "") === componentName
31340
- )?.name;
31341
- }
31342
- function getAllComponentNames() {
31343
- const staticUi = getStaticUiComponents();
31344
- const staticCustom = getStaticCustomComponents();
31345
- const templateKeys = Object.keys(TEMPLATE_REGISTRY);
31346
- return [.../* @__PURE__ */ new Set([...staticUi, ...staticCustom, ...templateKeys, "tiptap"])].sort();
31314
+ syncInstalledPresetManifests(cwd, config);
31315
+ syncInstalledIntegrationManifests(cwd, config);
31316
+ const projectPackageJson = readProjectPackageJson2(cwd);
31317
+ const missingPackageDependencies = Array.from(requiredPackageDependencies).filter(
31318
+ (dependency) => !hasDeclaredPackage2(projectPackageJson, dependency)
31319
+ );
31320
+ if (missingPackageDependencies.length > 0) {
31321
+ clack3.log.warn(
31322
+ `Updated components require package dependencies that are not declared: ${missingPackageDependencies.join(", ")}. Run 'pnpm betterstart update-deps' before building.`
31323
+ );
31324
+ }
31325
+ clack3.outro(
31326
+ `Updated ${updated} component${updated !== 1 ? "s" : ""}${skipped > 0 ? `, ${skipped} skipped` : ""}`
31327
+ );
31347
31328
  }
31348
- function getAllComponentNamesForConfig(config) {
31349
- const staticUi = getStaticUiComponents();
31350
- const staticCustom = getStaticCustomComponents();
31351
- const templateKeys = Object.entries(TEMPLATE_REGISTRY).filter(
31352
- ([, entry]) => (!entry.requiredPreset || hasPreset(config, entry.requiredPreset)) && (!entry.requiredIntegration || hasIntegration(config, entry.requiredIntegration))
31353
- ).map(([name]) => name);
31354
- return [.../* @__PURE__ */ new Set([...staticUi, ...staticCustom, ...templateKeys, "tiptap"])].sort();
31329
+ function removeLegacyDirectory(dirPath) {
31330
+ if (fs42.existsSync(dirPath)) {
31331
+ fs42.rmSync(dirPath, { recursive: true, force: true });
31332
+ }
31355
31333
  }
31356
- async function runUpdateCommand(components, options) {
31357
- const cwd = options.cwd ? path59.resolve(options.cwd) : process.cwd();
31358
- const normalizedOnly = normalizeShadcnPresetOnly(options.only);
31359
- validateShadcnPresetOptions(components, options);
31360
- if (options.json && !options.list) {
31361
- clack4.cancel("--json can only be used with --list.");
31334
+ function validateShadcnPresetOptions(components, options) {
31335
+ if (options.only && !options.shadcnPreset) {
31336
+ clack3.cancel("--only can only be used with --shadcn-preset.");
31362
31337
  process.exit(1);
31363
31338
  }
31339
+ if (!options.shadcnPreset) {
31340
+ return;
31341
+ }
31364
31342
  if (options.list) {
31365
- const uiComponents = getStaticUiComponentEntries();
31366
- const customComponents = getStaticCustomComponentEntries();
31367
- const templateKeys = Object.keys(TEMPLATE_REGISTRY).sort();
31368
- const templatePath = (name) => {
31369
- const entry = TEMPLATE_REGISTRY[name];
31370
- return entry.displayPath ?? (typeof entry.relPath === "string" ? entry.relPath : "");
31371
- };
31372
- if (options.json) {
31373
- const items = [
31374
- ...uiComponents.map((component) => ({
31375
- name: component.name,
31376
- path: `components/ui/${component.file}`,
31377
- kind: "ui"
31378
- })),
31379
- ...customComponents.map((component) => ({
31380
- name: component.name,
31381
- path: `components/custom/${component.file}`,
31382
- kind: "custom"
31383
- })),
31384
- ...templateKeys.map((name) => ({ name, path: templatePath(name), kind: "template" })),
31385
- { name: "tiptap", path: "components/custom/content-editor/tiptap-*/", kind: "special" }
31386
- ];
31387
- console.log(JSON.stringify(items, null, 2));
31388
- return;
31389
- }
31390
- const all = getAllComponentNames();
31391
- clack4.intro("Available components");
31392
- clack4.note(
31393
- renderTableRows(
31394
- uiComponents.map((component) => [component.name, `components/ui/${component.file}`])
31395
- ).join("\n"),
31396
- `Shadcn UI Components (${uiComponents.length})`
31343
+ clack3.cancel("--list cannot be combined with --shadcn-preset.");
31344
+ process.exit(1);
31345
+ }
31346
+ if (options.all) {
31347
+ clack3.cancel("--all cannot be combined with --shadcn-preset.");
31348
+ process.exit(1);
31349
+ }
31350
+ if (components.length > 0) {
31351
+ clack3.cancel("Component names cannot be combined with --shadcn-preset.");
31352
+ process.exit(1);
31353
+ }
31354
+ }
31355
+ function normalizeShadcnPresetOnly(value) {
31356
+ if (!value) {
31357
+ return void 0;
31358
+ }
31359
+ const parts = value.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
31360
+ if (parts.length !== 1 || parts[0] !== "theme") {
31361
+ clack3.cancel(
31362
+ "Admin-only shadcn preset updates currently support --only theme. Omit --only to apply the full preset."
31397
31363
  );
31398
- clack4.note(
31399
- renderTableRows(
31400
- customComponents.map((component) => [component.name, `components/custom/${component.file}`])
31401
- ).join("\n"),
31402
- `Custom Components (${customComponents.length})`
31364
+ process.exit(1);
31365
+ }
31366
+ return "theme";
31367
+ }
31368
+ function runShadcnPresetUpdate({
31369
+ cwd,
31370
+ config,
31371
+ preset,
31372
+ only
31373
+ }) {
31374
+ const namespace = resolveAdminNamespace(config.frameworkConfig.next.namespace);
31375
+ const adminGlobalsPath = path55.join(cwd, config.paths.admin, namespace.globalsFile);
31376
+ const componentsJsonPath = path55.join(cwd, "components.json");
31377
+ const shadcnBackupPath = `${componentsJsonPath}.bak`;
31378
+ const restoreAfterApplyPaths = [
31379
+ componentsJsonPath,
31380
+ shadcnBackupPath,
31381
+ path55.join(cwd, config.paths.admin, "lib", "utils.ts"),
31382
+ ...getHostProjectFilesToRestore(cwd)
31383
+ ];
31384
+ if (!preset) {
31385
+ clack3.cancel("--shadcn-preset requires a preset code, preset name, or preset URL.");
31386
+ process.exit(1);
31387
+ }
31388
+ if (!fs42.existsSync(adminGlobalsPath)) {
31389
+ clack3.cancel(
31390
+ `Admin globals file not found at ${path55.relative(cwd, adminGlobalsPath)}. Run 'betterstart update admin-globals' first.`
31403
31391
  );
31404
- clack4.note(
31405
- renderTableRows(templateKeys.map((name) => [name, templatePath(name)])).join("\n"),
31406
- `Template Components (${templateKeys.length})`
31392
+ process.exit(1);
31393
+ }
31394
+ const shadcnBin = resolveLocalShadcnBin(cwd);
31395
+ const restoreSnapshots = restoreAfterApplyPaths.map((filePath) => ({
31396
+ filePath,
31397
+ snapshot: snapshotFile(filePath)
31398
+ }));
31399
+ clack3.intro("BetterStart Shadcn Preset");
31400
+ clack3.log.info(`Applying preset to ${path55.join(config.paths.admin, "components/ui")}`);
31401
+ let failed = false;
31402
+ try {
31403
+ fs42.writeFileSync(
31404
+ componentsJsonPath,
31405
+ `${JSON.stringify(createAdminShadcnComponentsJson(config), null, 2)}
31406
+ `,
31407
+ "utf-8"
31407
31408
  );
31408
- clack4.note(
31409
- renderTableRows([["tiptap", "components/custom/content-editor/tiptap-*/ (all files)"]]).join(
31410
- "\n"
31411
- ),
31412
- "Special"
31409
+ const args = ["apply", "--preset", preset, "--cwd", cwd, "--yes"];
31410
+ if (only) {
31411
+ args.push("--only", only);
31412
+ }
31413
+ execFileSync5(shadcnBin, args, { cwd, stdio: "inherit" });
31414
+ } catch {
31415
+ failed = true;
31416
+ } finally {
31417
+ for (const { filePath, snapshot } of restoreSnapshots.reverse()) {
31418
+ restoreFile(filePath, snapshot);
31419
+ }
31420
+ }
31421
+ if (failed) {
31422
+ clack3.cancel("shadcn preset application failed.");
31423
+ process.exit(1);
31424
+ }
31425
+ clack3.outro(
31426
+ only ? `Applied shadcn preset parts (${only}) to the Admin.` : "Applied shadcn preset to Admin UI components and styles."
31427
+ );
31428
+ }
31429
+ function createAdminShadcnComponentsJson(config) {
31430
+ const adminPath = toPosixPath(config.paths.admin);
31431
+ const namespace = resolveAdminNamespace(config.frameworkConfig.next.namespace);
31432
+ return {
31433
+ $schema: "https://ui.shadcn.com/schema.json",
31434
+ style: "new-york",
31435
+ rsc: true,
31436
+ tsx: true,
31437
+ tailwind: {
31438
+ config: "",
31439
+ css: `${adminPath}/${namespace.globalsFile}`,
31440
+ baseColor: "neutral",
31441
+ cssVariables: true,
31442
+ prefix: ""
31443
+ },
31444
+ iconLibrary: "lucide",
31445
+ aliases: {
31446
+ components: `${namespace.alias}/components`,
31447
+ ui: `${namespace.alias}/components/ui`,
31448
+ hooks: `${namespace.alias}/hooks`,
31449
+ lib: `${namespace.alias}/lib`,
31450
+ utils: `${namespace.alias}/utils/shared/cn`
31451
+ }
31452
+ };
31453
+ }
31454
+ function toPosixPath(value) {
31455
+ return value.replace(/\\/g, "/");
31456
+ }
31457
+ function resolveLocalShadcnBin(cwd) {
31458
+ const binName = process.platform === "win32" ? "shadcn.cmd" : "shadcn";
31459
+ const shadcnBin = path55.join(cwd, "node_modules", ".bin", binName);
31460
+ if (!fs42.existsSync(shadcnBin)) {
31461
+ clack3.cancel(
31462
+ `shadcn is not installed in this project. Run 'betterstart update-deps' and try again.`
31413
31463
  );
31414
- clack4.outro(`${all.length} components available`);
31464
+ process.exit(1);
31465
+ }
31466
+ return shadcnBin;
31467
+ }
31468
+ function getHostProjectFilesToRestore(cwd) {
31469
+ const hostRelativePaths = [
31470
+ "app/layout.tsx",
31471
+ "app/layout.ts",
31472
+ "app/layout.jsx",
31473
+ "app/layout.js",
31474
+ "src/app/layout.tsx",
31475
+ "src/app/layout.ts",
31476
+ "src/app/layout.jsx",
31477
+ "src/app/layout.js",
31478
+ "app/globals.css",
31479
+ "src/app/globals.css"
31480
+ ];
31481
+ return hostRelativePaths.map((relativePath) => path55.join(cwd, relativePath));
31482
+ }
31483
+ function snapshotFile(filePath) {
31484
+ if (!fs42.existsSync(filePath)) {
31485
+ return { existed: false };
31486
+ }
31487
+ return { existed: true, content: fs42.readFileSync(filePath, "utf-8") };
31488
+ }
31489
+ function restoreFile(filePath, snapshot) {
31490
+ if (snapshot.existed) {
31491
+ fs42.writeFileSync(filePath, snapshot.content ?? "", "utf-8");
31415
31492
  return;
31416
31493
  }
31494
+ if (fs42.existsSync(filePath)) {
31495
+ fs42.rmSync(filePath, { force: true });
31496
+ }
31497
+ }
31498
+
31499
+ // adapters/next/commands/menu-choices.ts
31500
+ async function listInstallableChoices(cwd) {
31417
31501
  const config = await resolveConfigOrExit(cwd);
31418
- const admin = path59.resolve(cwd, config.paths.admin);
31419
- if (!fs45.existsSync(admin)) {
31420
- clack4.cancel(
31421
- `Admin directory not found at ${config.paths.admin}. Run 'betterstart init' first.`
31502
+ const installedPresets = new Set(config.presets.installed);
31503
+ const installedIntegrations = new Set(config.integrations.installed);
31504
+ return {
31505
+ presets: listAvailablePresets().map((preset) => ({
31506
+ id: preset.id,
31507
+ description: preset.description,
31508
+ installed: installedPresets.has(preset.id)
31509
+ })),
31510
+ integrations: listAvailableIntegrations().map((integration) => ({
31511
+ id: integration.id,
31512
+ description: integration.description,
31513
+ installed: installedIntegrations.has(integration.id)
31514
+ }))
31515
+ };
31516
+ }
31517
+ async function listSchemaChoices(cwd) {
31518
+ const config = await resolveConfigOrExit(cwd);
31519
+ const paths = resolveProjectPaths(config);
31520
+ return listSchemaNames(path56.join(cwd, ...paths.schemasDir.split("/")));
31521
+ }
31522
+ async function listComponentChoices(cwd) {
31523
+ const config = await resolveConfigOrExit(cwd);
31524
+ return getAllComponentNamesForConfig(config);
31525
+ }
31526
+
31527
+ // adapters/next/commands/remove.ts
31528
+ import path57 from "path";
31529
+ import * as p29 from "@clack/prompts";
31530
+ async function runRemoveCommand(items, options) {
31531
+ const removeIntegrationsMode = Boolean(options.integration);
31532
+ if (!removeIntegrationsMode && items.includes("core")) {
31533
+ p29.log.error("The core Admin cannot be removed.");
31534
+ process.exit(1);
31535
+ }
31536
+ const presetIds = items.filter(isPresetId);
31537
+ const integrationIds = items.filter(isIntegrationId);
31538
+ if (!removeIntegrationsMode && integrationIds.length > 0) {
31539
+ p29.log.error(
31540
+ `Integration IDs require --integration. Run \`betterstart remove --integration ${integrationIds.join(" ")}\`.`
31422
31541
  );
31423
31542
  process.exit(1);
31424
31543
  }
31425
- if (options.shadcnPreset) {
31426
- runShadcnPresetUpdate({
31427
- cwd,
31428
- config,
31429
- preset: options.shadcnPreset.trim(),
31430
- only: normalizedOnly
31431
- });
31432
- return;
31544
+ if (removeIntegrationsMode && presetIds.length > 0) {
31545
+ p29.log.error(`Preset IDs cannot be removed with --integration: ${presetIds.join(", ")}`);
31546
+ process.exit(1);
31433
31547
  }
31434
- if (!options.all && components.length === 0) {
31435
- clack4.log.error(
31436
- "Provide component names or use --all. Run with --list to see available components."
31548
+ const invalidItems = removeIntegrationsMode ? items.filter((integrationId) => !isIntegrationId(integrationId)) : items.filter((presetId) => !isPresetId(presetId));
31549
+ if (invalidItems.length > 0) {
31550
+ p29.log.error(
31551
+ removeIntegrationsMode ? formatUnknownIntegrationMessage(invalidItems) : formatUnknownPresetMessage(invalidItems)
31437
31552
  );
31438
31553
  process.exit(1);
31439
31554
  }
31440
- clack4.intro("BetterStart Update Components");
31441
- const toUpdate = options.all ? getAllComponentNamesForConfig(config) : components;
31442
- const uiDir = resolveCliAssetPath("shared-assets", "react-admin", "ui");
31443
- const customDir = resolveCliAssetPath("shared-assets", "react-admin", "custom");
31444
- let updated = 0;
31445
- let skipped = 0;
31446
- const updatedTemplateNames = /* @__PURE__ */ new Set();
31447
- const updatedStaticNames = /* @__PURE__ */ new Set();
31448
- const requiredPackageDependencies = /* @__PURE__ */ new Set();
31449
- const pendingWrites = [];
31450
- function trackPackageDependencies(name) {
31451
- for (const dependency of COMPONENT_PACKAGE_DEPENDENCIES[name] ?? []) {
31452
- requiredPackageDependencies.add(dependency);
31453
- }
31454
- }
31455
- function writeTemplateEntry(name, entry) {
31456
- if (updatedTemplateNames.has(name)) {
31457
- return false;
31458
- }
31459
- if (entry.requiredPreset && !hasPreset(config, entry.requiredPreset)) {
31460
- clack4.log.warn(`${name} requires the ${entry.requiredPreset} preset and was skipped.`);
31461
- skipped++;
31462
- return false;
31463
- }
31464
- if (entry.requiredIntegration && !hasIntegration(config, entry.requiredIntegration)) {
31465
- clack4.log.warn(
31466
- `${name} requires the ${entry.requiredIntegration} integration and was skipped.`
31467
- );
31468
- skipped++;
31469
- return false;
31470
- }
31471
- const { relPath, content } = applyNamespaceToTemplateEntry(entry, config, cwd);
31472
- const baseDir = entry.base === "cwd" ? cwd : admin;
31473
- const destPath = path59.join(baseDir, relPath);
31474
- if (entry.preserveExisting && fs45.existsSync(destPath)) {
31475
- clack4.log.info(`Preserved ${relPath}`);
31476
- updatedTemplateNames.add(name);
31477
- skipped++;
31478
- return false;
31479
- }
31480
- pendingWrites.push({
31481
- displayPath: relPath,
31482
- write: () => {
31483
- fsExtra.ensureDirSync(path59.dirname(destPath));
31484
- fs45.writeFileSync(destPath, content, "utf-8");
31485
- clack4.log.success(`Updated ${relPath}`);
31486
- }
31555
+ const cwd = options.cwd ? path57.resolve(options.cwd) : process.cwd();
31556
+ const config = await resolveConfigOrExit(cwd);
31557
+ const pm = detectPackageManager(cwd);
31558
+ if (!options.force) {
31559
+ const confirmed = await p29.confirm({
31560
+ message: `Remove ${removeIntegrationsMode ? "integration" : "preset"}${items.length === 1 ? "" : "s"} ${items.join(", ")}?`,
31561
+ initialValue: false
31487
31562
  });
31488
- updatedTemplateNames.add(name);
31489
- trackPackageDependencies(name);
31490
- updated++;
31491
- return true;
31492
- }
31493
- function writeTemplateEntryWithDependencies(name, entry) {
31494
- const wroteEntry = writeTemplateEntry(name, entry);
31495
- if (!wroteEntry) {
31496
- return false;
31497
- }
31498
- for (const dependencyName of entry.dependencies ?? []) {
31499
- writeNamedDependency(dependencyName);
31563
+ if (p29.isCancel(confirmed) || !confirmed) {
31564
+ p29.cancel(`${removeIntegrationsMode ? "Integration" : "Preset"} removal cancelled.`);
31565
+ process.exit(0);
31500
31566
  }
31501
- return true;
31502
31567
  }
31503
- function writeStaticAssetEntry(assetDirectory, name) {
31504
- const key = `${assetDirectory}:${name}`;
31505
- if (updatedStaticNames.has(key)) {
31506
- return false;
31507
- }
31508
- const assetDir = assetDirectory === "ui" ? uiDir : customDir;
31509
- const assetFile = findStaticAssetFile(assetDir, name);
31510
- if (!assetFile) {
31511
- return false;
31512
- }
31513
- const namespace = config.frameworkConfig.next.namespace;
31514
- const namespacedAssetFile = applyAdminNamespaceToPath(assetFile, namespace);
31515
- const destPath = path59.join(admin, "components", assetDirectory, namespacedAssetFile);
31516
- pendingWrites.push({
31517
- displayPath: `components/${assetDirectory}/${namespacedAssetFile}`,
31518
- write: () => {
31519
- fsExtra.ensureDirSync(path59.dirname(destPath));
31520
- writeNamespacedFile(path59.join(assetDir, assetFile), destPath, namespace);
31521
- clack4.log.success(`Updated components/${assetDirectory}/${namespacedAssetFile}`);
31522
- }
31568
+ if (removeIntegrationsMode) {
31569
+ const result2 = await removeIntegrations({
31570
+ cwd,
31571
+ config,
31572
+ pm,
31573
+ integrationIds
31523
31574
  });
31524
- if (assetDirectory === "custom") {
31525
- const assetSubdir = path59.join(assetDir, name);
31526
- if (fs45.existsSync(assetSubdir) && fs45.statSync(assetSubdir).isDirectory()) {
31527
- const namespacedName = applyAdminNamespaceToPath(name, namespace);
31528
- const destSubdir = path59.join(admin, "components", assetDirectory, namespacedName);
31529
- pendingWrites.push({
31530
- displayPath: `components/${assetDirectory}/${namespacedName}/ (template files)`,
31531
- write: () => {
31532
- fsExtra.ensureDirSync(destSubdir);
31533
- copyNamespacedDirectory(assetSubdir, destSubdir, namespace);
31534
- clack4.log.success(
31535
- `Updated components/${assetDirectory}/${namespacedName}/ (template files)`
31536
- );
31537
- }
31538
- });
31539
- }
31540
- }
31541
- updatedStaticNames.add(key);
31542
- trackPackageDependencies(name);
31543
- updated++;
31544
- if (assetDirectory === "custom") {
31545
- for (const dependencyName of STATIC_CUSTOM_DEPENDENCIES[name] ?? []) {
31546
- writeNamedDependency(dependencyName);
31547
- }
31575
+ writeConfigFile(cwd, result2.config);
31576
+ if (result2.removed.length === 0) {
31577
+ p29.outro("No integrations were removed.");
31578
+ return;
31548
31579
  }
31549
- return true;
31550
- }
31551
- function writeTiptapTemplates() {
31552
- const key = "custom:tiptap";
31553
- if (updatedStaticNames.has(key)) {
31554
- return true;
31580
+ if (result2.warnings.length > 0) {
31581
+ p29.note(result2.warnings.join("\n"), "Warnings");
31555
31582
  }
31556
- const srcBaseDir = resolveCliAssetPath(
31557
- "shared-assets",
31558
- "react-admin",
31559
- "custom",
31560
- "content-editor"
31583
+ p29.outro(
31584
+ `Removed integration${result2.removed.length === 1 ? "" : "s"}: ${result2.removed.join(", ")}`
31561
31585
  );
31562
- const namespace = config.frameworkConfig.next.namespace;
31563
- const destBaseDir = path59.join(admin, "components", "custom", "content-editor");
31564
- if (!fs45.existsSync(srcBaseDir)) {
31565
- return false;
31566
- }
31567
- const dirsToCopy = [];
31568
- for (const directory of TIPTAP_CONTENT_EDITOR_DIRECTORIES) {
31569
- const srcDir = path59.join(srcBaseDir, directory);
31570
- if (!fs45.existsSync(srcDir)) {
31586
+ return;
31587
+ }
31588
+ const result = await removePresets({
31589
+ cwd,
31590
+ config,
31591
+ pm,
31592
+ presetIds
31593
+ });
31594
+ writeConfigFile(cwd, result.config);
31595
+ if (result.removed.length === 0) {
31596
+ p29.outro("No presets were removed.");
31597
+ return;
31598
+ }
31599
+ if (result.warnings.length > 0) {
31600
+ p29.note(result.warnings.join("\n"), "Warnings");
31601
+ }
31602
+ p29.outro(`Removed preset${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`);
31603
+ }
31604
+
31605
+ // adapters/next/commands/remove-schema.ts
31606
+ import fs43 from "fs";
31607
+ import path58 from "path";
31608
+ import * as clack4 from "@clack/prompts";
31609
+ function removePath2(cwd, filePath) {
31610
+ const fullPath = path58.join(cwd, ...filePath.split("/"));
31611
+ const existed = fs43.existsSync(fullPath);
31612
+ fs43.rmSync(fullPath, { recursive: true, force: true });
31613
+ return existed;
31614
+ }
31615
+ function cleanupEmptyDirs3(cwd, deletedPaths, configPaths) {
31616
+ const stopRoots = /* @__PURE__ */ new Set([
31617
+ path58.join(cwd, ...configPaths.adminDir.split("/")),
31618
+ path58.join(cwd, ...configPaths.adminNavigationDir.split("/")),
31619
+ path58.join(cwd, ...configPaths.adminWebhookEventsDir.split("/")),
31620
+ path58.join(cwd, ...configPaths.pagesDir.split("/"))
31621
+ ]);
31622
+ for (const deletedPath of deletedPaths) {
31623
+ let current = path58.dirname(path58.join(cwd, ...deletedPath.split("/")));
31624
+ while (!stopRoots.has(current)) {
31625
+ if (!fs43.existsSync(current)) {
31626
+ current = path58.dirname(current);
31571
31627
  continue;
31572
31628
  }
31573
- dirsToCopy.push({
31574
- srcDir,
31575
- destDir: path59.join(destBaseDir, applyAdminNamespaceToPath(directory, namespace))
31576
- });
31577
- }
31578
- if (dirsToCopy.length === 0) {
31579
- return false;
31580
- }
31581
- pendingWrites.push({
31582
- displayPath: "components/custom/content-editor/tiptap-*/ (template files)",
31583
- write: () => {
31584
- for (const { srcDir, destDir } of dirsToCopy) {
31585
- fsExtra.ensureDirSync(destDir);
31586
- copyNamespacedDirectory(srcDir, destDir, namespace);
31587
- }
31588
- clack4.log.success("Updated components/custom/content-editor/tiptap-*/ (template files)");
31589
- removeLegacyDirectory(path59.join(admin, "components", "custom", "tiptap"));
31590
- }
31591
- });
31592
- updatedStaticNames.add(key);
31593
- trackPackageDependencies("tiptap");
31594
- updated++;
31595
- for (const dependencyName of TIPTAP_TEMPLATE_DEPENDENCIES) {
31596
- writeNamedDependency(dependencyName);
31597
- }
31598
- return true;
31599
- }
31600
- function writeNamedDependency(name) {
31601
- const entry = TEMPLATE_REGISTRY[name];
31602
- if (entry) {
31603
- writeTemplateEntryWithDependencies(name, entry);
31604
- return;
31629
+ const entries = fs43.readdirSync(current);
31630
+ if (entries.length > 0) {
31631
+ break;
31632
+ }
31633
+ fs43.rmdirSync(current);
31634
+ current = path58.dirname(current);
31605
31635
  }
31606
- if (writeStaticAssetEntry("ui", name)) {
31607
- return;
31636
+ }
31637
+ }
31638
+ function resolveSchemaOwnerForRemoval(cwd, schemaName) {
31639
+ const explicitOwner = getSchemaOwner(cwd, schemaName);
31640
+ if (explicitOwner) {
31641
+ return explicitOwner;
31642
+ }
31643
+ if (schemaName === "settings") {
31644
+ return "core";
31645
+ }
31646
+ return "user";
31647
+ }
31648
+ async function runRemoveSchemaCommand(schemaName, options) {
31649
+ const owner = resolveSchemaOwnerForRemoval(
31650
+ options.cwd ? path58.resolve(options.cwd) : process.cwd(),
31651
+ schemaName
31652
+ );
31653
+ if (owner === "core") {
31654
+ clack4.log.error(
31655
+ `"${schemaName}" is a core Admin module and cannot be removed with remove-schema.`
31656
+ );
31657
+ process.exit(1);
31658
+ }
31659
+ if (owner.startsWith("preset:")) {
31660
+ clack4.log.error(`"${schemaName}" is owned by ${owner}. Remove the owning preset instead.`);
31661
+ process.exit(1);
31662
+ }
31663
+ const cwd = options.cwd ? path58.resolve(options.cwd) : process.cwd();
31664
+ const config = await resolveConfigOrExit(cwd);
31665
+ const paths = resolveProjectPaths(config);
31666
+ const manifest = loadManifest(cwd, schemaName);
31667
+ if (!snapshotRootExists(cwd)) {
31668
+ clack4.log.error(UNSUPPORTED_PROJECT_STATE_MESSAGE);
31669
+ process.exit(1);
31670
+ }
31671
+ if (!manifest) {
31672
+ clack4.log.error(UNSUPPORTED_PROJECT_STATE_MESSAGE);
31673
+ process.exit(1);
31674
+ }
31675
+ if (!options.force) {
31676
+ if (!isInteractiveSession()) {
31677
+ clack4.log.error(
31678
+ `Removing generated files for ${schemaName} needs confirmation. Re-run with --force.`
31679
+ );
31680
+ process.exit(1);
31608
31681
  }
31609
- if (writeStaticAssetEntry("custom", name)) {
31682
+ const confirmed = await clack4.confirm({
31683
+ message: `Remove generated files for ${schemaName}?`,
31684
+ initialValue: false
31685
+ });
31686
+ if (clack4.isCancel(confirmed) || !confirmed) {
31687
+ clack4.cancel("Cancelled.");
31610
31688
  return;
31611
31689
  }
31612
- if (name === "tiptap") {
31613
- writeTiptapTemplates();
31614
- }
31615
31690
  }
31616
- for (const name of toUpdate) {
31617
- if (TEMPLATE_REGISTRY[name]) {
31618
- const entry = TEMPLATE_REGISTRY[name];
31619
- writeTemplateEntryWithDependencies(name, entry);
31620
- continue;
31621
- }
31622
- if (writeStaticAssetEntry("ui", name)) {
31623
- continue;
31691
+ const deletedPaths = [];
31692
+ for (const file of [...manifest.files.map((entry) => entry.path), ...manifest.skipped]) {
31693
+ if (removePath2(cwd, file)) {
31694
+ deletedPaths.push(file);
31624
31695
  }
31625
- if (writeStaticAssetEntry("custom", name)) {
31626
- continue;
31696
+ }
31697
+ const loaded = (() => {
31698
+ try {
31699
+ return loadSchema(path58.join(cwd, ...paths.schemasDir.split("/")), schemaName);
31700
+ } catch {
31701
+ return null;
31627
31702
  }
31628
- if (name === "tiptap") {
31629
- if (!writeTiptapTemplates()) {
31630
- clack4.log.warn("tiptap templates not found");
31631
- skipped++;
31632
- }
31633
- continue;
31703
+ })();
31704
+ const kebabName = toKebabCase(schemaName);
31705
+ if (loaded?.type === "form") {
31706
+ if (removePath2(cwd, `${paths.adminActionsDir}/${schemaName}`)) {
31707
+ deletedPaths.push(`${paths.adminActionsDir}/${schemaName}`);
31634
31708
  }
31635
- clack4.log.warn(`Unknown component: ${name}`);
31636
- skipped++;
31637
- }
31638
- if (pendingWrites.length > 0) {
31639
- const displayPaths = pendingWrites.map((entry) => entry.displayPath);
31640
- const preview = displayPaths.slice(0, MAX_OVERWRITE_PREVIEW_ROWS);
31641
- if (displayPaths.length > preview.length) {
31642
- preview.push(`\u2026 and ${displayPaths.length - preview.length} more`);
31709
+ if (removePath2(cwd, `${paths.pagesDir}/forms/${kebabName}`)) {
31710
+ deletedPaths.push(`${paths.pagesDir}/forms/${kebabName}`);
31643
31711
  }
31644
- clack4.note(
31645
- preview.join("\n"),
31646
- `Overwriting ${displayPaths.length} file${displayPaths.length === 1 ? "" : "s"}`
31647
- );
31648
- if (!options.yes) {
31649
- if (!isInteractiveSession()) {
31650
- clack4.log.error(
31651
- `Overwriting ${displayPaths.length} file${displayPaths.length === 1 ? "" : "s"} needs confirmation. Re-run with --yes.`
31652
- );
31653
- process.exit(1);
31654
- }
31655
- const proceed = await clack4.confirm({
31656
- message: "Overwrite these files with the latest templates?",
31657
- initialValue: true
31658
- });
31659
- if (clack4.isCancel(proceed) || !proceed) {
31660
- clack4.cancel("Update cancelled.");
31661
- process.exit(0);
31662
- }
31712
+ } else {
31713
+ if (removePath2(cwd, `${paths.adminActionsDir}/${schemaName}`)) {
31714
+ deletedPaths.push(`${paths.adminActionsDir}/${schemaName}`);
31663
31715
  }
31664
- for (const entry of pendingWrites) {
31665
- entry.write();
31716
+ if (removePath2(cwd, `${paths.pagesDir}/${schemaName}`)) {
31717
+ deletedPaths.push(`${paths.pagesDir}/${schemaName}`);
31666
31718
  }
31667
31719
  }
31668
- syncInstalledPresetManifests(cwd, config);
31669
- syncInstalledIntegrationManifests(cwd, config);
31670
- const projectPackageJson = readProjectPackageJson2(cwd);
31671
- const missingPackageDependencies = Array.from(requiredPackageDependencies).filter(
31672
- (dependency) => !hasDeclaredPackage2(projectPackageJson, dependency)
31673
- );
31674
- if (missingPackageDependencies.length > 0) {
31675
- clack4.log.warn(
31676
- `Updated components require package dependencies that are not declared: ${missingPackageDependencies.join(", ")}. Run 'pnpm betterstart update-deps' before building.`
31677
- );
31720
+ cleanupEmptyDirs3(cwd, deletedPaths, paths);
31721
+ deleteSnapshot(cwd, schemaName);
31722
+ if (hasTombstone(cwd, schemaName)) {
31723
+ clearTombstone(cwd, schemaName);
31678
31724
  }
31679
- clack4.outro(
31680
- `Updated ${updated} component${updated !== 1 ? "s" : ""}${skipped > 0 ? `, ${skipped} skipped` : ""}`
31725
+ writeTombstone(cwd, schemaName);
31726
+ await applyGeneratedFiles({
31727
+ cwd,
31728
+ config,
31729
+ scope: BARREL_SCOPE,
31730
+ schemaJson: { name: BARREL_SCOPE },
31731
+ generatedFiles: renderBarrelFiles(cwd, config),
31732
+ force: false,
31733
+ interactive: false
31734
+ });
31735
+ clack4.log.info(
31736
+ `Tombstone written: .betterstart/snapshots/_removed/${schemaName}
31737
+ Schema JSON preserved.`
31681
31738
  );
31739
+ clack4.outro(`Removed generated files for ${schemaName}`);
31682
31740
  }
31683
- function removeLegacyDirectory(dirPath) {
31684
- if (fs45.existsSync(dirPath)) {
31685
- fs45.rmSync(dirPath, { recursive: true, force: true });
31741
+
31742
+ // adapters/next/commands/uninstall.ts
31743
+ import fs45 from "fs";
31744
+ import path59 from "path";
31745
+ import * as p30 from "@clack/prompts";
31746
+ import pc11 from "picocolors";
31747
+
31748
+ // adapters/next/commands/uninstall-cleaners.ts
31749
+ import fs44 from "fs";
31750
+ function stripJsonComments2(input) {
31751
+ let result = "";
31752
+ let i = 0;
31753
+ while (i < input.length) {
31754
+ if (input[i] === '"') {
31755
+ let j = i + 1;
31756
+ while (j < input.length) {
31757
+ if (input[j] === "\\") {
31758
+ j += 2;
31759
+ continue;
31760
+ }
31761
+ if (input[j] === '"') {
31762
+ j++;
31763
+ break;
31764
+ }
31765
+ j++;
31766
+ }
31767
+ result += input.slice(i, j);
31768
+ i = j;
31769
+ } else if (input[i] === "/" && input[i + 1] === "/") {
31770
+ const nl = input.indexOf("\n", i);
31771
+ i = nl === -1 ? input.length : nl;
31772
+ } else if (input[i] === "/" && input[i + 1] === "*") {
31773
+ const end = input.indexOf("*/", i + 2);
31774
+ i = end === -1 ? input.length : end + 2;
31775
+ } else {
31776
+ result += input[i];
31777
+ i++;
31778
+ }
31686
31779
  }
31780
+ return result;
31687
31781
  }
31688
- function validateShadcnPresetOptions(components, options) {
31689
- if (options.only && !options.shadcnPreset) {
31690
- clack4.cancel("--only can only be used with --shadcn-preset.");
31691
- process.exit(1);
31782
+ function cleanTsconfig(tsconfigPath, aliasRoot = "@admin") {
31783
+ if (!fs44.existsSync(tsconfigPath)) return [];
31784
+ const raw = fs44.readFileSync(tsconfigPath, "utf-8");
31785
+ const stripped = stripJsonComments2(raw).replace(/,\s*([\]}])/g, "$1");
31786
+ let tsconfig;
31787
+ try {
31788
+ tsconfig = JSON.parse(stripped);
31789
+ } catch {
31790
+ return [];
31692
31791
  }
31693
- if (!options.shadcnPreset) {
31694
- return;
31792
+ const compilerOptions = tsconfig.compilerOptions ?? {};
31793
+ const paths = compilerOptions.paths ?? {};
31794
+ const removed = [];
31795
+ for (const key of Object.keys(paths)) {
31796
+ if (key.startsWith("@admin/") || key === "@admin/*" || key.startsWith(`${aliasRoot}/`) || key === `${aliasRoot}/*`) {
31797
+ removed.push(key);
31798
+ delete paths[key];
31799
+ }
31695
31800
  }
31696
- if (options.list) {
31697
- clack4.cancel("--list cannot be combined with --shadcn-preset.");
31698
- process.exit(1);
31801
+ if (removed.length === 0) return [];
31802
+ if (Object.keys(paths).length === 0) {
31803
+ compilerOptions.paths = void 0;
31804
+ } else {
31805
+ compilerOptions.paths = paths;
31699
31806
  }
31700
- if (options.all) {
31701
- clack4.cancel("--all cannot be combined with --shadcn-preset.");
31702
- process.exit(1);
31807
+ tsconfig.compilerOptions = compilerOptions;
31808
+ fs44.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
31809
+ `, "utf-8");
31810
+ return removed;
31811
+ }
31812
+ function cleanCss(cssPath, namespace = "admin") {
31813
+ if (!fs44.existsSync(cssPath)) return [];
31814
+ const content = fs44.readFileSync(cssPath, "utf-8");
31815
+ const lines = content.split("\n");
31816
+ const sourcePattern = new RegExp(`^@source\\s+"[^"]*(?:admin|${namespace})[^"]*";\\s*$`);
31817
+ const removed = [];
31818
+ const kept = [];
31819
+ for (const line of lines) {
31820
+ if (sourcePattern.test(line)) {
31821
+ removed.push(line.trim());
31822
+ } else {
31823
+ kept.push(line);
31824
+ }
31703
31825
  }
31704
- if (components.length > 0) {
31705
- clack4.cancel("Component names cannot be combined with --shadcn-preset.");
31706
- process.exit(1);
31826
+ if (removed.length === 0) return [];
31827
+ const cleaned = kept.join("\n").replace(/\n{3,}/g, "\n\n");
31828
+ fs44.writeFileSync(cssPath, cleaned, "utf-8");
31829
+ return removed;
31830
+ }
31831
+ function cleanEnvFile(envPath) {
31832
+ if (!fs44.existsSync(envPath)) return [];
31833
+ const content = fs44.readFileSync(envPath, "utf-8");
31834
+ const lines = content.split("\n");
31835
+ const removed = [];
31836
+ const kept = [];
31837
+ const headerPattern = /^# =+$/;
31838
+ const headerTextPattern = /^# BetterStart Admin$/;
31839
+ for (let i = 0; i < lines.length; i++) {
31840
+ const line = lines[i];
31841
+ const trimmed = line.trim();
31842
+ if (trimmed.match(/^BETTERSTART_\w+=/)) {
31843
+ const key = trimmed.split("=")[0];
31844
+ removed.push(key);
31845
+ continue;
31846
+ }
31847
+ if (headerPattern.test(trimmed)) {
31848
+ const next = lines[i + 1]?.trim();
31849
+ const afterNext = lines[i + 2]?.trim();
31850
+ if (next && headerTextPattern.test(next) && afterNext && headerPattern.test(afterNext)) {
31851
+ i += 2;
31852
+ continue;
31853
+ }
31854
+ }
31855
+ if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
31856
+ const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
31857
+ if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
31858
+ continue;
31859
+ }
31860
+ }
31861
+ kept.push(line);
31862
+ }
31863
+ if (removed.length === 0) return [];
31864
+ const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
31865
+ if (result === "") {
31866
+ fs44.unlinkSync(envPath);
31867
+ } else {
31868
+ fs44.writeFileSync(envPath, `${result}
31869
+ `, "utf-8");
31707
31870
  }
31871
+ return removed;
31708
31872
  }
31709
- function normalizeShadcnPresetOnly(value) {
31710
- if (!value) {
31711
- return void 0;
31712
- }
31713
- const parts = value.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
31714
- if (parts.length !== 1 || parts[0] !== "theme") {
31715
- clack4.cancel(
31716
- "Admin-only shadcn preset updates currently support --only theme. Omit --only to apply the full preset."
31717
- );
31718
- process.exit(1);
31873
+ function findNextNonEmptyLine(lines, startIndex) {
31874
+ for (let i = startIndex; i < lines.length; i++) {
31875
+ const trimmed = lines[i].trim();
31876
+ if (trimmed !== "") return trimmed;
31719
31877
  }
31720
- return "theme";
31878
+ return null;
31721
31879
  }
31722
- function runShadcnPresetUpdate({
31723
- cwd,
31724
- config,
31725
- preset,
31726
- only
31727
- }) {
31728
- const namespace = resolveAdminNamespace(config.frameworkConfig.next.namespace);
31729
- const adminGlobalsPath = path59.join(cwd, config.paths.admin, namespace.globalsFile);
31730
- const componentsJsonPath = path59.join(cwd, "components.json");
31731
- const shadcnBackupPath = `${componentsJsonPath}.bak`;
31732
- const restoreAfterApplyPaths = [
31733
- componentsJsonPath,
31734
- shadcnBackupPath,
31735
- path59.join(cwd, config.paths.admin, "lib", "utils.ts"),
31736
- ...getHostProjectFilesToRestore(cwd)
31880
+
31881
+ // adapters/next/commands/uninstall.ts
31882
+ function findMainCss2(cwd) {
31883
+ const candidates = [
31884
+ "src/app/globals.css",
31885
+ "app/globals.css",
31886
+ "src/app/global.css",
31887
+ "app/global.css",
31888
+ "src/app/app.css",
31889
+ "app/app.css",
31890
+ "src/globals.css",
31891
+ "globals.css"
31737
31892
  ];
31738
- if (!preset) {
31739
- clack4.cancel("--shadcn-preset requires a preset code, preset name, or preset URL.");
31740
- process.exit(1);
31741
- }
31742
- if (!fs45.existsSync(adminGlobalsPath)) {
31743
- clack4.cancel(
31744
- `Admin globals file not found at ${path59.relative(cwd, adminGlobalsPath)}. Run 'betterstart update admin-globals' first.`
31745
- );
31746
- process.exit(1);
31893
+ for (const candidate of candidates) {
31894
+ const filePath = path59.join(cwd, candidate);
31895
+ if (fs45.existsSync(filePath)) return filePath;
31747
31896
  }
31748
- const shadcnBin = resolveLocalShadcnBin(cwd);
31749
- const restoreSnapshots = restoreAfterApplyPaths.map((filePath) => ({
31750
- filePath,
31751
- snapshot: snapshotFile(filePath)
31752
- }));
31753
- clack4.intro("BetterStart Shadcn Preset");
31754
- clack4.log.info(`Applying preset to ${path59.join(config.paths.admin, "components/ui")}`);
31755
- let failed = false;
31897
+ return void 0;
31898
+ }
31899
+ function isCLICreatedBiome(biomePath) {
31900
+ if (!fs45.existsSync(biomePath)) return false;
31756
31901
  try {
31757
- fs45.writeFileSync(
31758
- componentsJsonPath,
31759
- `${JSON.stringify(createAdminShadcnComponentsJson(config), null, 2)}
31760
- `,
31761
- "utf-8"
31762
- );
31763
- const args = ["apply", "--preset", preset, "--cwd", cwd, "--yes"];
31764
- if (only) {
31765
- args.push("--only", only);
31766
- }
31767
- execFileSync5(shadcnBin, args, { cwd, stdio: "inherit" });
31902
+ const content = JSON.parse(fs45.readFileSync(biomePath, "utf-8"));
31903
+ return content.$schema?.includes("biomejs.dev") && content.formatter?.indentStyle === "space" && content.javascript?.formatter?.quoteStyle === "single" && Array.isArray(content.files?.ignore) && content.files.ignore.includes(".next");
31768
31904
  } catch {
31769
- failed = true;
31770
- } finally {
31771
- for (const { filePath, snapshot } of restoreSnapshots.reverse()) {
31772
- restoreFile(filePath, snapshot);
31905
+ return false;
31906
+ }
31907
+ }
31908
+ function buildUninstallPlan(cwd, namespaceValue) {
31909
+ const steps = [];
31910
+ const namespace = resolveAdminNamespace(namespaceValue);
31911
+ const hasSrc = fs45.existsSync(path59.join(cwd, "src"));
31912
+ const appBase = hasSrc ? "src/app" : "app";
31913
+ const dirs = [];
31914
+ const adminDir = path59.join(cwd, namespace.segment);
31915
+ const legacyAdminDir = path59.join(cwd, "admin");
31916
+ const adminRouteGroup = path59.join(cwd, appBase, namespace.routeGroup);
31917
+ const legacyAdminRouteGroup = path59.join(cwd, appBase, "(admin)");
31918
+ if (fs45.existsSync(adminDir)) dirs.push(`${namespace.segment}/`);
31919
+ if (namespace.segment !== "admin" && fs45.existsSync(legacyAdminDir)) dirs.push("admin/");
31920
+ if (fs45.existsSync(adminRouteGroup)) dirs.push(`${appBase}/${namespace.routeGroup}/`);
31921
+ if (namespace.segment !== "admin" && fs45.existsSync(legacyAdminRouteGroup))
31922
+ dirs.push(`${appBase}/(admin)/`);
31923
+ if (dirs.length > 0) {
31924
+ steps.push({
31925
+ label: "Admin directories",
31926
+ items: dirs,
31927
+ count: dirs.length,
31928
+ unit: dirs.length === 1 ? "directory" : "directories",
31929
+ execute() {
31930
+ if (fs45.existsSync(adminDir)) fs45.rmSync(adminDir, { recursive: true, force: true });
31931
+ if (fs45.existsSync(legacyAdminDir))
31932
+ fs45.rmSync(legacyAdminDir, { recursive: true, force: true });
31933
+ if (fs45.existsSync(adminRouteGroup)) {
31934
+ fs45.rmSync(adminRouteGroup, { recursive: true, force: true });
31935
+ }
31936
+ if (fs45.existsSync(legacyAdminRouteGroup)) {
31937
+ fs45.rmSync(legacyAdminRouteGroup, { recursive: true, force: true });
31938
+ }
31939
+ }
31940
+ });
31941
+ }
31942
+ const configFiles = [];
31943
+ const configPaths = [];
31944
+ const candidates = [
31945
+ [CONFIG_FILE_NAME, path59.join(cwd, CONFIG_FILE_NAME)],
31946
+ ["drizzle.config.ts", path59.join(cwd, "drizzle.config.ts")],
31947
+ ["ADMIN.md", path59.join(cwd, "ADMIN.md")]
31948
+ ];
31949
+ for (const [label, fullPath] of candidates) {
31950
+ if (fs45.existsSync(fullPath)) {
31951
+ configFiles.push(label);
31952
+ configPaths.push(fullPath);
31773
31953
  }
31774
31954
  }
31775
- if (failed) {
31776
- clack4.cancel("shadcn preset application failed.");
31777
- process.exit(1);
31955
+ const biomePath = path59.join(cwd, "biome.json");
31956
+ if (isCLICreatedBiome(biomePath)) {
31957
+ configFiles.push("biome.json (CLI-created)");
31958
+ configPaths.push(biomePath);
31778
31959
  }
31779
- clack4.outro(
31780
- only ? `Applied shadcn preset parts (${only}) to the Admin.` : "Applied shadcn preset to Admin UI components and styles."
31781
- );
31782
- }
31783
- function createAdminShadcnComponentsJson(config) {
31784
- const adminPath = toPosixPath(config.paths.admin);
31785
- const namespace = resolveAdminNamespace(config.frameworkConfig.next.namespace);
31786
- return {
31787
- $schema: "https://ui.shadcn.com/schema.json",
31788
- style: "new-york",
31789
- rsc: true,
31790
- tsx: true,
31791
- tailwind: {
31792
- config: "",
31793
- css: `${adminPath}/${namespace.globalsFile}`,
31794
- baseColor: "neutral",
31795
- cssVariables: true,
31796
- prefix: ""
31797
- },
31798
- iconLibrary: "lucide",
31799
- aliases: {
31800
- components: `${namespace.alias}/components`,
31801
- ui: `${namespace.alias}/components/ui`,
31802
- hooks: `${namespace.alias}/hooks`,
31803
- lib: `${namespace.alias}/lib`,
31804
- utils: `${namespace.alias}/utils/shared/cn`
31960
+ if (configFiles.length > 0) {
31961
+ steps.push({
31962
+ label: "Config files",
31963
+ items: configFiles,
31964
+ count: configFiles.length,
31965
+ unit: configFiles.length === 1 ? "file" : "files",
31966
+ execute() {
31967
+ for (const p32 of configPaths) {
31968
+ if (fs45.existsSync(p32)) fs45.unlinkSync(p32);
31969
+ }
31970
+ }
31971
+ });
31972
+ }
31973
+ const tsconfigPath = path59.join(cwd, "tsconfig.json");
31974
+ if (fs45.existsSync(tsconfigPath)) {
31975
+ const content = fs45.readFileSync(tsconfigPath, "utf-8");
31976
+ const aliasMatches = [
31977
+ ...content.match(/"@admin\//g) ?? [],
31978
+ ...content.match(new RegExp(`"${namespace.alias}/`, "g")) ?? []
31979
+ ];
31980
+ if (aliasMatches && aliasMatches.length > 0) {
31981
+ const aliasCount = aliasMatches.length;
31982
+ steps.push({
31983
+ label: "tsconfig.json path aliases",
31984
+ items: [`${namespace.alias}/* aliases in tsconfig.json`],
31985
+ count: aliasCount,
31986
+ unit: aliasCount === 1 ? "alias" : "aliases",
31987
+ execute() {
31988
+ cleanTsconfig(tsconfigPath, namespace.alias);
31989
+ }
31990
+ });
31805
31991
  }
31806
- };
31807
- }
31808
- function toPosixPath(value) {
31809
- return value.replace(/\\/g, "/");
31810
- }
31811
- function resolveLocalShadcnBin(cwd) {
31812
- const binName = process.platform === "win32" ? "shadcn.cmd" : "shadcn";
31813
- const shadcnBin = path59.join(cwd, "node_modules", ".bin", binName);
31814
- if (!fs45.existsSync(shadcnBin)) {
31815
- clack4.cancel(
31816
- `shadcn is not installed in this project. Run 'betterstart update-deps' and try again.`
31992
+ }
31993
+ const cssFile = findMainCss2(cwd);
31994
+ if (cssFile) {
31995
+ const cssContent = fs45.readFileSync(cssFile, "utf-8");
31996
+ const sourceLines = cssContent.split("\n").filter(
31997
+ (l) => new RegExp(`^@source\\s+"[^"]*(?:admin|${namespace.segment})[^"]*";\\s*$`).test(l)
31817
31998
  );
31818
- process.exit(1);
31999
+ if (sourceLines.length > 0) {
32000
+ const relCss = path59.relative(cwd, cssFile);
32001
+ steps.push({
32002
+ label: `CSS @source lines (${relCss})`,
32003
+ items: [`@source lines in ${relCss}`],
32004
+ count: sourceLines.length,
32005
+ unit: sourceLines.length === 1 ? "line" : "lines",
32006
+ execute() {
32007
+ cleanCss(cssFile, namespace.segment);
32008
+ }
32009
+ });
32010
+ }
31819
32011
  }
31820
- return shadcnBin;
31821
- }
31822
- function getHostProjectFilesToRestore(cwd) {
31823
- const hostRelativePaths = [
31824
- "app/layout.tsx",
31825
- "app/layout.ts",
31826
- "app/layout.jsx",
31827
- "app/layout.js",
31828
- "src/app/layout.tsx",
31829
- "src/app/layout.ts",
31830
- "src/app/layout.jsx",
31831
- "src/app/layout.js",
31832
- "app/globals.css",
31833
- "src/app/globals.css"
31834
- ];
31835
- return hostRelativePaths.map((relativePath) => path59.join(cwd, relativePath));
31836
- }
31837
- function snapshotFile(filePath) {
31838
- if (!fs45.existsSync(filePath)) {
31839
- return { existed: false };
32012
+ const envPath = path59.join(cwd, ".env.local");
32013
+ if (fs45.existsSync(envPath)) {
32014
+ const envContent = fs45.readFileSync(envPath, "utf-8");
32015
+ const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
32016
+ if (bsVars.length > 0) {
32017
+ steps.push({
32018
+ label: ".env.local variables",
32019
+ items: ["BETTERSTART_* vars in .env.local"],
32020
+ count: bsVars.length,
32021
+ unit: bsVars.length === 1 ? "variable" : "variables",
32022
+ execute() {
32023
+ cleanEnvFile(envPath);
32024
+ }
32025
+ });
32026
+ }
31840
32027
  }
31841
- return { existed: true, content: fs45.readFileSync(filePath, "utf-8") };
32028
+ return steps;
31842
32029
  }
31843
- function restoreFile(filePath, snapshot) {
31844
- if (snapshot.existed) {
31845
- fs45.writeFileSync(filePath, snapshot.content ?? "", "utf-8");
32030
+ async function runUninstallCommand(options) {
32031
+ const cwd = options.cwd ? path59.resolve(options.cwd) : process.cwd();
32032
+ p30.intro(pc11.bgRed(pc11.white(" BetterStart Uninstall ")));
32033
+ let namespace = DEFAULT_ADMIN_NAMESPACE;
32034
+ try {
32035
+ const config = await resolveConfig(cwd);
32036
+ namespace = config.frameworkConfig.next.namespace;
32037
+ } catch {
32038
+ }
32039
+ const steps = buildUninstallPlan(cwd, namespace);
32040
+ if (steps.length === 0) {
32041
+ p30.log.success(`${pc11.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
32042
+ p30.outro("Project already clean");
31846
32043
  return;
31847
32044
  }
31848
- if (fs45.existsSync(filePath)) {
31849
- fs45.rmSync(filePath, { force: true });
32045
+ const planLines = steps.map((step) => {
32046
+ const names = step.items.join(" ");
32047
+ const countLabel = pc11.dim(`${step.count} ${step.unit}`);
32048
+ return `${pc11.red("\u2717")} ${names} ${countLabel}`;
32049
+ });
32050
+ p30.note(planLines.join("\n"), "Uninstall plan");
32051
+ if (!options.force) {
32052
+ const confirmed = await p30.confirm({
32053
+ message: "Proceed with uninstall?",
32054
+ initialValue: false
32055
+ });
32056
+ if (p30.isCancel(confirmed) || !confirmed) {
32057
+ p30.cancel("Uninstall cancelled.");
32058
+ process.exit(0);
32059
+ }
32060
+ }
32061
+ const s = spinner2();
32062
+ s.start(steps[0].label);
32063
+ for (const step of steps) {
32064
+ s.message(step.label);
32065
+ step.execute();
31850
32066
  }
32067
+ const parts = steps.map((step) => `${step.count} ${step.unit}`);
32068
+ s.stop(`Removed ${parts.join(", ")}`);
32069
+ p30.note(pc11.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
32070
+ p30.outro("Uninstall complete");
31851
32071
  }
31852
32072
 
31853
32073
  // adapters/next/commands/update-deps.ts
@@ -31913,6 +32133,7 @@ async function runUpdateStylesCommand(options) {
31913
32133
 
31914
32134
  // adapters/next/commands-runtime.ts
31915
32135
  var nextCommandRuntime = {
32136
+ listComponentChoices,
31916
32137
  listInstallableChoices,
31917
32138
  listSchemaChoices,
31918
32139
  runAdd: runAddCommand,