@betterstart/cli 0.1.27 → 0.1.28

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
@@ -6561,29 +6561,29 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
6561
6561
  icon: schema.icon
6562
6562
  };
6563
6563
  if (schema.navGroup) {
6564
- let group = items.find((item) => item.label === schema.navGroup?.label);
6565
- if (!group) {
6566
- group = {
6564
+ let group2 = items.find((item) => item.label === schema.navGroup?.label);
6565
+ if (!group2) {
6566
+ group2 = {
6567
6567
  label: schema.navGroup.label,
6568
6568
  href: "#",
6569
6569
  icon: schema.navGroup.icon,
6570
6570
  children: []
6571
6571
  };
6572
- items.push(group);
6572
+ items.push(group2);
6573
6573
  }
6574
- if (!group.children) {
6575
- group.children = [];
6574
+ if (!group2.children) {
6575
+ group2.children = [];
6576
6576
  }
6577
- const existingChild = group.children.findIndex((c) => c.href === entityHref);
6577
+ const existingChild = group2.children.findIndex((c) => c.href === entityHref);
6578
6578
  if (existingChild >= 0) {
6579
6579
  if (options.force) {
6580
- group.children[existingChild] = newItem;
6580
+ group2.children[existingChild] = newItem;
6581
6581
  } else {
6582
6582
  return { files: [] };
6583
6583
  }
6584
6584
  } else {
6585
- group.children.push(newItem);
6586
- group.children.sort((a, b) => a.label.localeCompare(b.label));
6585
+ group2.children.push(newItem);
6586
+ group2.children.sort((a, b) => a.label.localeCompare(b.label));
6587
6587
  }
6588
6588
  if (schema.navGroup.icon && !iconImports.includes(schema.navGroup.icon)) {
6589
6589
  iconImports.push(schema.navGroup.icon);
@@ -7972,14 +7972,6 @@ function openBrowser(url) {
7972
7972
  // src/init/prompts/features.ts
7973
7973
  import * as p2 from "@clack/prompts";
7974
7974
  async function promptFeatures(presetOverride) {
7975
- const includeEmail = await p2.confirm({
7976
- message: "Include email system? (Resend + React Email)",
7977
- initialValue: true
7978
- });
7979
- if (p2.isCancel(includeEmail)) {
7980
- p2.cancel("Setup cancelled.");
7981
- process.exit(0);
7982
- }
7983
7975
  let preset;
7984
7976
  if (presetOverride && isValidPreset(presetOverride)) {
7985
7977
  preset = presetOverride;
@@ -7999,7 +7991,7 @@ async function promptFeatures(presetOverride) {
7999
7991
  }
8000
7992
  preset = selected;
8001
7993
  }
8002
- return { includeEmail, preset };
7994
+ return { includeEmail: true, preset };
8003
7995
  }
8004
7996
  function isValidPreset(value) {
8005
7997
  return value === "blank" || value === "blog" || value === "full";
@@ -13633,13 +13625,13 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13633
13625
  }
13634
13626
  });
13635
13627
  });
13636
- const spinner4 = clack.spinner();
13637
- spinner4.start("Creating admin user...");
13628
+ const spinner5 = clack.spinner();
13629
+ spinner5.start("Creating admin user...");
13638
13630
  try {
13639
13631
  const result = await runSeed2(false);
13640
13632
  if (result.code === 2) {
13641
13633
  const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
13642
- spinner4.stop(`Account already exists for ${email}`);
13634
+ spinner5.stop(`Account already exists for ${email}`);
13643
13635
  const overwrite = await clack.confirm({
13644
13636
  message: `An admin account (${existingName}) already exists with this email. Replace it?`
13645
13637
  });
@@ -13651,14 +13643,14 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13651
13643
  }
13652
13644
  process.exit(0);
13653
13645
  }
13654
- spinner4.start("Replacing admin user...");
13646
+ spinner5.start("Replacing admin user...");
13655
13647
  await runSeed2(true);
13656
- spinner4.stop("Admin user replaced");
13648
+ spinner5.stop("Admin user replaced");
13657
13649
  } else {
13658
- spinner4.stop("Admin user created");
13650
+ spinner5.stop("Admin user created");
13659
13651
  }
13660
13652
  } catch (err) {
13661
- spinner4.stop("Failed to create admin user");
13653
+ spinner5.stop("Failed to create admin user");
13662
13654
  const errMsg = err instanceof Error ? err.message : String(err);
13663
13655
  clack.log.error(errMsg);
13664
13656
  clack.log.info("You can run the seed script manually:");
@@ -13691,21 +13683,19 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13691
13683
  let isFreshProject = false;
13692
13684
  let srcDir;
13693
13685
  if (project.isExisting) {
13694
- p4.log.info(`Existing Next.js project detected`);
13695
- p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
13686
+ p4.log.info(`Next.js project detected ${pc2.dim("\xB7")} ${pc2.cyan(pm)}`);
13696
13687
  srcDir = project.hasSrcDir;
13697
13688
  if (!project.hasTypeScript) {
13698
13689
  p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13699
13690
  process.exit(1);
13700
13691
  }
13701
13692
  if (project.conflicts.length > 0) {
13702
- p4.log.error("Conflicts detected:");
13703
- for (const conflict of project.conflicts) {
13704
- p4.log.warning(` - ${conflict}`);
13705
- }
13693
+ const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
13694
+ conflictLines.push("", pc2.dim("Existing files will not be overwritten."));
13695
+ p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
13706
13696
  if (!options.yes) {
13707
13697
  const proceed = await p4.confirm({
13708
- message: "Continue anyway? (existing files will NOT be overwritten)",
13698
+ message: "Continue anyway?",
13709
13699
  initialValue: true
13710
13700
  });
13711
13701
  if (p4.isCancel(proceed) || !proceed) {
@@ -13814,30 +13804,62 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13814
13804
  ...getDefaultConfig(srcDir),
13815
13805
  features: { email: features.includeEmail }
13816
13806
  };
13807
+ const results = [];
13817
13808
  const s = p4.spinner();
13818
- s.start("Creating CMS directory structure...");
13809
+ s.start("Directory structure");
13819
13810
  const baseFiles = scaffoldBase({ cwd, config });
13820
- s.stop(`Created ${baseFiles.length} files`);
13821
- s.start("Configuring TypeScript path aliases...");
13811
+ results.push({ label: "Directory structure", result: `${baseFiles.length} files` });
13812
+ s.message("TypeScript aliases");
13822
13813
  const tsResult = scaffoldTsconfig(cwd);
13823
- s.stop(`Added ${tsResult.added.length} path aliases`);
13824
- s.start("Configuring Tailwind CSS...");
13814
+ results.push({
13815
+ label: "TypeScript aliases",
13816
+ result: tsResult.added.length > 0 ? `${tsResult.added.length} paths` : "already set"
13817
+ });
13818
+ s.message("Tailwind CSS");
13825
13819
  const twResult = scaffoldTailwind(cwd, srcDir);
13826
- if (twResult.appended) {
13827
- s.stop(`Updated ${twResult.file}`);
13828
- } else if (twResult.file) {
13829
- s.stop("Tailwind already configured for CMS");
13830
- } else {
13831
- s.stop("No CSS file found (will configure later)");
13832
- }
13833
- s.start("Setting up environment variables...");
13820
+ results.push({
13821
+ label: "Tailwind CSS",
13822
+ result: twResult.appended ? "updated" : twResult.file ? "already set" : "no CSS file"
13823
+ });
13824
+ s.message("Environment variables");
13834
13825
  const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
13835
- const envParts = [`Added ${envResult.added.length}`];
13836
- if (envResult.updated.length > 0) envParts.push(`updated ${envResult.updated.length}`);
13837
- s.stop(`${envParts.join(", ")} env vars in .env.local`);
13838
- s.start("Setting up database...");
13826
+ const envCount = envResult.added.length + envResult.updated.length;
13827
+ results.push({
13828
+ label: "Environment variables",
13829
+ result: envCount > 0 ? `${envCount} vars` : "already set"
13830
+ });
13831
+ s.message("Database");
13839
13832
  const dbFiles = scaffoldDatabase({ cwd, config });
13840
- s.stop(`Created ${dbFiles.length} database files`);
13833
+ results.push({ label: "Database", result: `${dbFiles.length} files` });
13834
+ s.message("Authentication");
13835
+ const authFiles = scaffoldAuth({ cwd, config });
13836
+ results.push({ label: "Authentication", result: `${authFiles.length} files` });
13837
+ s.message("Components");
13838
+ const compFiles = scaffoldComponents({ cwd, config });
13839
+ results.push({ label: "Components", result: `${compFiles.length} files` });
13840
+ s.message("Pages & layouts");
13841
+ const layoutFiles = scaffoldLayout({ cwd, config });
13842
+ results.push({ label: "Pages & layouts", result: `${layoutFiles.length} files` });
13843
+ s.message("API routes");
13844
+ const apiFiles = scaffoldApiRoutes({ cwd, config });
13845
+ results.push({ label: "API routes", result: `${apiFiles.length} routes` });
13846
+ s.message("Linter");
13847
+ let linterResult;
13848
+ if (project.linter.type === "none") {
13849
+ const biomeResult = scaffoldBiome(cwd, project.linter);
13850
+ linterResult = biomeResult.installed ? "biome (new)" : "none";
13851
+ } else {
13852
+ linterResult = project.linter.type;
13853
+ }
13854
+ results.push({ label: "Linter", result: linterResult });
13855
+ const maxLabel = Math.max(...results.map((r) => r.label.length));
13856
+ const noteLines = results.map((r) => {
13857
+ const padded = r.label.padEnd(maxLabel + 3);
13858
+ return `${pc2.green("\u2713")} ${padded}${pc2.dim(r.result)}`;
13859
+ });
13860
+ s.stop("");
13861
+ process.stdout.write("\x1B[2A\x1B[J");
13862
+ p4.note(noteLines.join("\n"), "Scaffolded CMS");
13841
13863
  const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
13842
13864
  if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
13843
13865
  if (!options.yes) {
@@ -13852,42 +13874,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13852
13874
  }
13853
13875
  }
13854
13876
  }
13855
- s.start("Setting up authentication...");
13856
- const authFiles = scaffoldAuth({ cwd, config });
13857
- s.stop(`Created ${authFiles.length} auth files`);
13858
- s.start("Copying CMS components...");
13859
- const compFiles = scaffoldComponents({ cwd, config });
13860
- s.stop(`Created ${compFiles.length} component files`);
13861
- s.start("Creating CMS pages and layouts...");
13862
- const layoutFiles = scaffoldLayout({ cwd, config });
13863
- s.stop(`Created ${layoutFiles.length} page files`);
13864
- s.start("Creating API routes...");
13865
- const apiFiles = scaffoldApiRoutes({ cwd, config });
13866
- s.stop(`Created ${apiFiles.length} API routes`);
13867
- s.start("Checking for linter...");
13868
- if (project.linter.type === "none") {
13869
- s.stop("No linter found");
13870
- s.start("Setting up Biome linter...");
13871
- const biomeResult = scaffoldBiome(cwd, project.linter);
13872
- if (biomeResult.installed) {
13873
- s.stop("Created biome.json");
13874
- } else {
13875
- s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13876
- }
13877
- } else {
13878
- s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
13879
- }
13880
- s.start("Installing dependencies (this may take a minute)...");
13877
+ s.start("Installing dependencies (this may take a minute)");
13881
13878
  const depsResult = await installDependenciesAsync({
13882
13879
  cwd,
13883
13880
  pm,
13884
13881
  includeEmail: features.includeEmail,
13885
13882
  includeBiome: project.linter.type === "none"
13886
13883
  });
13884
+ let depsInstalled = false;
13887
13885
  if (depsResult.success) {
13888
- s.stop(
13889
- `Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`
13890
- );
13886
+ s.stop("");
13887
+ depsInstalled = true;
13891
13888
  } else {
13892
13889
  s.stop("Failed to install dependencies");
13893
13890
  p4.log.warning(depsResult.error ?? "Unknown error");
@@ -13897,24 +13894,59 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13897
13894
  ${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13898
13895
  );
13899
13896
  }
13900
- s.start(`Applying ${features.preset} preset...`);
13897
+ if (depsInstalled) {
13898
+ process.stdout.write("\x1B[2A\x1B[J");
13899
+ }
13900
+ s.start(`Applying ${features.preset} preset`);
13901
13901
  const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13902
+ {
13903
+ const entityNames = [];
13904
+ const formNames = [];
13905
+ const schemasDir = path37.join(cwd, config.paths.schemas);
13906
+ const formsDir = path37.join(schemasDir, "forms");
13907
+ if (fs32.existsSync(schemasDir)) {
13908
+ for (const f of fs32.readdirSync(schemasDir)) {
13909
+ if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13910
+ }
13911
+ }
13912
+ if (fs32.existsSync(formsDir)) {
13913
+ for (const f of fs32.readdirSync(formsDir)) {
13914
+ if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13915
+ }
13916
+ }
13917
+ regenerateCmsDoc(cwd, config, {
13918
+ preset: features.preset,
13919
+ schemas: entityNames,
13920
+ forms: formNames
13921
+ });
13922
+ }
13923
+ s.stop("");
13924
+ process.stdout.write("\x1B[2A\x1B[J");
13925
+ const installLines = [];
13926
+ if (depsInstalled) {
13927
+ installLines.push(
13928
+ `${pc2.green("\u2713")} Dependencies ${pc2.dim(`${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`)}`
13929
+ );
13930
+ }
13902
13931
  if (presetResult.errors.length > 0) {
13903
- s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13932
+ installLines.push(
13933
+ `${pc2.yellow("\u25B2")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.errors.length} warning(s)`)}`
13934
+ );
13904
13935
  for (const err of presetResult.errors) {
13905
- p4.log.warning(` ${err}`);
13936
+ installLines.push(` ${pc2.dim(err)}`);
13906
13937
  }
13907
13938
  } else {
13908
- s.stop(
13909
- `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13939
+ installLines.push(
13940
+ `${pc2.green("\u2713")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.schemas.length} schemas, ${presetResult.generatedFiles.length} files`)}`
13910
13941
  );
13911
13942
  }
13943
+ p4.note(installLines.join("\n"), "Installed");
13912
13944
  let dbPushed = false;
13913
13945
  if (depsResult.success && hasDbUrl(cwd)) {
13914
- s.start("Pushing database schema (drizzle-kit push)...");
13946
+ s.start("Pushing database schema (drizzle-kit push)");
13915
13947
  const pushResult = await runDrizzlePush(cwd);
13916
13948
  if (pushResult.success) {
13917
- s.stop("Database schema pushed");
13949
+ s.stop(`${pc2.green("\u2713")} Database schema pushed`);
13918
13950
  dbPushed = true;
13919
13951
  } else {
13920
13952
  s.stop("Database push failed");
@@ -13926,66 +13958,62 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13926
13958
  let seedPassword;
13927
13959
  let seedSuccess = false;
13928
13960
  if (dbPushed && !options.yes) {
13929
- p4.log.step("Create your admin account");
13930
- const email = await p4.text({
13931
- message: "Admin email",
13932
- placeholder: "admin@example.com",
13933
- validate: (v) => {
13934
- if (!v || !v.includes("@")) return "Please enter a valid email";
13961
+ p4.note(pc2.dim("Create your first admin user to access the CMS."), "Admin account");
13962
+ const credentials = await p4.group(
13963
+ {
13964
+ email: () => p4.text({
13965
+ message: "Admin email",
13966
+ placeholder: "admin@example.com",
13967
+ validate: (v) => {
13968
+ if (!v || !v.includes("@")) return "Please enter a valid email";
13969
+ }
13970
+ }),
13971
+ password: () => p4.password({
13972
+ message: "Admin password",
13973
+ validate: (v) => {
13974
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
13975
+ }
13976
+ })
13977
+ },
13978
+ {
13979
+ onCancel: () => {
13980
+ p4.cancel("Setup cancelled.");
13981
+ process.exit(0);
13982
+ }
13935
13983
  }
13936
- });
13937
- if (p4.isCancel(email)) {
13938
- p4.cancel("Setup cancelled.");
13939
- process.exit(0);
13940
- }
13941
- const password3 = await p4.password({
13942
- message: "Admin password",
13943
- validate: (v) => {
13944
- if (!v || v.length < 8) return "Password must be at least 8 characters";
13984
+ );
13985
+ seedEmail = credentials.email;
13986
+ seedPassword = credentials.password;
13987
+ s.start("Creating admin user");
13988
+ let seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password);
13989
+ if (seedResult.existingUser) {
13990
+ s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
13991
+ const replace = await p4.confirm({
13992
+ message: "Replace existing admin user?",
13993
+ initialValue: false
13994
+ });
13995
+ if (!p4.isCancel(replace) && replace) {
13996
+ s.start("Replacing admin user");
13997
+ seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password, true);
13998
+ } else {
13999
+ seedSuccess = true;
13945
14000
  }
13946
- });
13947
- if (p4.isCancel(password3)) {
13948
- p4.cancel("Setup cancelled.");
13949
- process.exit(0);
13950
14001
  }
13951
- seedEmail = email;
13952
- seedPassword = password3;
13953
- s.start("Creating admin user...");
13954
- const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
13955
14002
  if (seedResult.success) {
13956
- s.stop("Admin user created");
14003
+ s.stop(`${pc2.green("\u2713")} Admin user created`);
13957
14004
  seedSuccess = true;
13958
- } else {
13959
- s.stop("Failed to create admin user");
13960
- p4.log.warning(seedResult.error ?? "Unknown error");
13961
- p4.log.info(`You can run it manually: ${pc2.cyan("npx betterstart seed")}`);
13962
- }
13963
- }
13964
- s.start("Generating documentation...");
13965
- {
13966
- const entityNames = [];
13967
- const formNames = [];
13968
- const schemasDir = path37.join(cwd, config.paths.schemas);
13969
- const formsDir = path37.join(schemasDir, "forms");
13970
- if (fs32.existsSync(schemasDir)) {
13971
- for (const f of fs32.readdirSync(schemasDir)) {
13972
- if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13973
- }
13974
- }
13975
- if (fs32.existsSync(formsDir)) {
13976
- for (const f of fs32.readdirSync(formsDir)) {
13977
- if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13978
- }
14005
+ } else if (!seedSuccess && seedResult.error) {
14006
+ s.stop(`${pc2.red("\u2717")} Failed to create admin user`);
14007
+ p4.note(
14008
+ `${pc2.red(seedResult.error)}
14009
+
14010
+ Run manually: ${pc2.cyan("npx betterstart seed")}`,
14011
+ pc2.red("Seed failed")
14012
+ );
13979
14013
  }
13980
- regenerateCmsDoc(cwd, config, {
13981
- preset: features.preset,
13982
- schemas: entityNames,
13983
- forms: formNames
13984
- });
13985
14014
  }
13986
- s.stop("Generated CMS.md");
13987
14015
  if (isFreshProject) {
13988
- s.start("Creating initial git commit...");
14016
+ s.start("Creating initial git commit");
13989
14017
  try {
13990
14018
  execFileSync4("git", ["init"], { cwd, stdio: "pipe" });
13991
14019
  execFileSync4("git", ["add", "."], { cwd, stdio: "pipe" });
@@ -14001,7 +14029,6 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
14001
14029
  const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
14002
14030
  const summaryLines = [
14003
14031
  `Preset: ${pc2.cyan(features.preset)}`,
14004
- `Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
14005
14032
  `Files created: ${pc2.cyan(String(totalFiles))}`,
14006
14033
  `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
14007
14034
  ];
@@ -14091,26 +14118,14 @@ function hasDbUrl(cwd) {
14091
14118
  }
14092
14119
  return false;
14093
14120
  }
14094
- async function runSeed(cwd, cmsDir, email, password3) {
14121
+ function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
14095
14122
  const scriptsDir = path37.join(cwd, cmsDir, "scripts");
14096
14123
  const seedPath = path37.join(scriptsDir, "seed.ts");
14097
14124
  if (!fs32.existsSync(scriptsDir)) {
14098
14125
  fs32.mkdirSync(scriptsDir, { recursive: true });
14099
14126
  }
14100
14127
  fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
14101
- try {
14102
- const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
14103
- execFileSync4(tsxBin, [seedPath], {
14104
- cwd,
14105
- stdio: "pipe",
14106
- timeout: 3e4,
14107
- env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
14108
- });
14109
- return { success: true, error: null };
14110
- } catch (err) {
14111
- const msg = err instanceof Error ? err.message : String(err);
14112
- return { success: false, error: msg };
14113
- } finally {
14128
+ const cleanup = () => {
14114
14129
  try {
14115
14130
  fs32.unlinkSync(seedPath);
14116
14131
  if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
@@ -14118,7 +14133,71 @@ async function runSeed(cwd, cmsDir, email, password3) {
14118
14133
  }
14119
14134
  } catch {
14120
14135
  }
14121
- }
14136
+ };
14137
+ return new Promise((resolve) => {
14138
+ const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
14139
+ const child = spawn2(tsxBin, [seedPath], {
14140
+ cwd,
14141
+ stdio: "pipe",
14142
+ env: {
14143
+ ...process.env,
14144
+ SEED_EMAIL: email,
14145
+ SEED_PASSWORD: password3,
14146
+ SEED_NAME: "Admin",
14147
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
14148
+ }
14149
+ });
14150
+ let stdout = "";
14151
+ let stderr = "";
14152
+ child.stdout?.on("data", (chunk) => {
14153
+ stdout += chunk.toString();
14154
+ });
14155
+ child.stderr?.on("data", (chunk) => {
14156
+ stderr += chunk.toString();
14157
+ });
14158
+ const timeout = setTimeout(() => {
14159
+ child.kill();
14160
+ cleanup();
14161
+ resolve({ success: false, error: "Seed timed out after 30 seconds" });
14162
+ }, 3e4);
14163
+ child.on("close", (code) => {
14164
+ clearTimeout(timeout);
14165
+ cleanup();
14166
+ if (code === 0) {
14167
+ resolve({ success: true, error: null });
14168
+ } else if (code === 2) {
14169
+ const name = stdout.match(/EXISTING_USER:(.+)/)?.[1]?.trim();
14170
+ resolve({ success: false, error: null, existingUser: name ?? email });
14171
+ } else {
14172
+ resolve({ success: false, error: parseSeedError(stdout, stderr) });
14173
+ }
14174
+ });
14175
+ child.on("error", (err) => {
14176
+ clearTimeout(timeout);
14177
+ cleanup();
14178
+ resolve({ success: false, error: parseSeedError("", err.message) });
14179
+ });
14180
+ });
14181
+ }
14182
+ function parseSeedError(stdout, stderr) {
14183
+ const combined = `${stdout}
14184
+ ${stderr}`;
14185
+ const seedFailed = combined.match(/Seed failed:\s*(.+)/)?.[1]?.trim();
14186
+ if (seedFailed) return seedFailed;
14187
+ if (combined.includes("Failed to create user")) return "Auth API failed to create user";
14188
+ if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
14189
+ return "Could not connect to database";
14190
+ if (combined.includes("BETTERSTART_DATABASE_URL"))
14191
+ return "Database URL is missing or invalid";
14192
+ if (combined.includes("password authentication failed"))
14193
+ return "Database authentication failed \u2014 check your connection string";
14194
+ if (combined.includes("does not exist") && combined.includes("relation"))
14195
+ return "Database tables not found \u2014 run npx drizzle-kit push first";
14196
+ if (combined.includes("MODULE_NOT_FOUND") || combined.includes("Cannot find module"))
14197
+ return "Missing dependencies \u2014 run your package manager install first";
14198
+ const firstLine = stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("at ") && !l.startsWith("node:"));
14199
+ if (firstLine) return firstLine;
14200
+ return "Unknown error \u2014 run npx betterstart seed for details";
14122
14201
  }
14123
14202
  function runDrizzlePush(cwd) {
14124
14203
  return new Promise((resolve) => {
@@ -14573,6 +14652,8 @@ function buildUninstallPlan(cwd) {
14573
14652
  steps.push({
14574
14653
  label: "CMS directories",
14575
14654
  items: dirs,
14655
+ count: dirs.length,
14656
+ unit: dirs.length === 1 ? "directory" : "directories",
14576
14657
  execute() {
14577
14658
  if (fs35.existsSync(cmsDir)) fs35.rmSync(cmsDir, { recursive: true, force: true });
14578
14659
  if (fs35.existsSync(cmsRouteGroup)) fs35.rmSync(cmsRouteGroup, { recursive: true, force: true });
@@ -14601,6 +14682,8 @@ function buildUninstallPlan(cwd) {
14601
14682
  steps.push({
14602
14683
  label: "Config files",
14603
14684
  items: configFiles,
14685
+ count: configFiles.length,
14686
+ unit: configFiles.length === 1 ? "file" : "files",
14604
14687
  execute() {
14605
14688
  for (const p6 of configPaths) {
14606
14689
  if (fs35.existsSync(p6)) fs35.unlinkSync(p6);
@@ -14611,10 +14694,14 @@ function buildUninstallPlan(cwd) {
14611
14694
  const tsconfigPath = path40.join(cwd, "tsconfig.json");
14612
14695
  if (fs35.existsSync(tsconfigPath)) {
14613
14696
  const content = fs35.readFileSync(tsconfigPath, "utf-8");
14614
- if (content.includes("@cms/")) {
14697
+ const aliasMatches = content.match(/"@cms\//g);
14698
+ if (aliasMatches && aliasMatches.length > 0) {
14699
+ const aliasCount = aliasMatches.length;
14615
14700
  steps.push({
14616
14701
  label: "tsconfig.json path aliases",
14617
- items: ["Remove all @cms/* paths from compilerOptions.paths"],
14702
+ items: [`@cms/* aliases in tsconfig.json`],
14703
+ count: aliasCount,
14704
+ unit: aliasCount === 1 ? "alias" : "aliases",
14618
14705
  execute() {
14619
14706
  cleanTsconfig(tsconfigPath);
14620
14707
  }
@@ -14624,12 +14711,14 @@ function buildUninstallPlan(cwd) {
14624
14711
  const cssFile = findMainCss2(cwd);
14625
14712
  if (cssFile) {
14626
14713
  const cssContent = fs35.readFileSync(cssFile, "utf-8");
14627
- const sourcePattern = /^@source\s+"[^"]*cms[^"]*";\s*$/m;
14628
- if (sourcePattern.test(cssContent)) {
14714
+ const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14715
+ if (sourceLines.length > 0) {
14629
14716
  const relCss = path40.relative(cwd, cssFile);
14630
14717
  steps.push({
14631
14718
  label: `CSS @source lines (${relCss})`,
14632
- items: ["Remove @source lines referencing cms/"],
14719
+ items: [`@source lines in ${relCss}`],
14720
+ count: sourceLines.length,
14721
+ unit: sourceLines.length === 1 ? "line" : "lines",
14633
14722
  execute() {
14634
14723
  cleanCss(cssFile);
14635
14724
  }
@@ -14643,7 +14732,9 @@ function buildUninstallPlan(cwd) {
14643
14732
  if (bsVars.length > 0) {
14644
14733
  steps.push({
14645
14734
  label: ".env.local variables",
14646
- items: bsVars,
14735
+ items: ["BETTERSTART_* vars in .env.local"],
14736
+ count: bsVars.length,
14737
+ unit: bsVars.length === 1 ? "variable" : "variables",
14647
14738
  execute() {
14648
14739
  cleanEnvFile(envPath);
14649
14740
  }
@@ -14657,21 +14748,19 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14657
14748
  p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14658
14749
  const steps = buildUninstallPlan(cwd);
14659
14750
  if (steps.length === 0) {
14660
- p5.log.info("Nothing to remove \u2014 project is already clean.");
14751
+ p5.log.success(pc3.green("\u2713") + " Nothing to remove \u2014 project is already clean.");
14661
14752
  p5.outro("Done");
14662
14753
  return;
14663
14754
  }
14664
- p5.log.warn("The following will be removed/modified:\n");
14665
- for (const step of steps) {
14666
- p5.log.message(` ${pc3.bold(step.label)}`);
14667
- for (const item of step.items) {
14668
- p5.log.message(` ${pc3.red("\xD7")} ${pc3.dim(item)}`);
14669
- }
14670
- p5.log.message("");
14671
- }
14755
+ const planLines = steps.map((step) => {
14756
+ const names = step.items.join(" ");
14757
+ const countLabel = pc3.dim(`${step.count} ${step.unit}`);
14758
+ return `${pc3.red("\xD7")} ${names} ${countLabel}`;
14759
+ });
14760
+ p5.note(planLines.join("\n"), "Uninstall plan");
14672
14761
  if (!options.force) {
14673
14762
  const confirmed = await p5.confirm({
14674
- message: `Proceed with uninstall? (${steps.length} ${steps.length === 1 ? "area" : "areas"})`,
14763
+ message: "Proceed with uninstall?",
14675
14764
  initialValue: false
14676
14765
  });
14677
14766
  if (p5.isCancel(confirmed) || !confirmed) {
@@ -14679,29 +14768,19 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14679
14768
  process.exit(0);
14680
14769
  }
14681
14770
  }
14682
- let completedCount = 0;
14771
+ const s = p5.spinner();
14772
+ s.start(steps[0].label);
14683
14773
  for (const step of steps) {
14774
+ s.message(step.label);
14684
14775
  step.execute();
14685
- completedCount++;
14686
- p5.log.success(`Removed: ${step.label}`);
14687
- }
14688
- p5.log.message("");
14689
- if (completedCount === 0) {
14690
- p5.log.info("No changes were made.");
14691
- } else {
14692
- p5.note(
14693
- pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
14694
- "Next steps"
14695
- );
14696
14776
  }
14697
- if (findMainCss2(cwd)) {
14698
- p5.log.info(
14699
- pc3.dim(
14700
- "Note: @theme tokens were left in your CSS \u2014 they're harmless and may be shared with your own styles."
14701
- )
14702
- );
14703
- }
14704
- p5.outro(completedCount > 0 ? "Uninstall complete" : "Done");
14777
+ const parts = steps.map((step) => `${step.count} ${step.unit}`);
14778
+ s.stop(`Removed ${parts.join(", ")}`);
14779
+ p5.note(
14780
+ pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
14781
+ "Next steps"
14782
+ );
14783
+ p5.outro("Uninstall complete");
14705
14784
  });
14706
14785
 
14707
14786
  // src/commands/update-styles.ts