@betterstart/cli 0.1.26 → 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);
@@ -7640,9 +7640,6 @@ function detectPackageManager(cwd) {
7640
7640
  }
7641
7641
  return "npm";
7642
7642
  }
7643
- function installCommand(pm) {
7644
- return pm === "yarn" ? "yarn" : `${pm} install`;
7645
- }
7646
7643
  function runCommand(pm, script) {
7647
7644
  switch (pm) {
7648
7645
  case "pnpm":
@@ -7975,14 +7972,6 @@ function openBrowser(url) {
7975
7972
  // src/init/prompts/features.ts
7976
7973
  import * as p2 from "@clack/prompts";
7977
7974
  async function promptFeatures(presetOverride) {
7978
- const includeEmail = await p2.confirm({
7979
- message: "Include email system? (Resend + React Email)",
7980
- initialValue: true
7981
- });
7982
- if (p2.isCancel(includeEmail)) {
7983
- p2.cancel("Setup cancelled.");
7984
- process.exit(0);
7985
- }
7986
7975
  let preset;
7987
7976
  if (presetOverride && isValidPreset(presetOverride)) {
7988
7977
  preset = presetOverride;
@@ -8002,7 +7991,7 @@ async function promptFeatures(presetOverride) {
8002
7991
  }
8003
7992
  preset = selected;
8004
7993
  }
8005
- return { includeEmail, preset };
7994
+ return { includeEmail: true, preset };
8006
7995
  }
8007
7996
  function isValidPreset(value) {
8008
7997
  return value === "blank" || value === "blog" || value === "full";
@@ -11678,7 +11667,6 @@ var CORE_DEPS = [
11678
11667
  "input-otp",
11679
11668
  "react-resizable-panels",
11680
11669
  "recharts",
11681
- "shadcn",
11682
11670
  "tw-animate-css",
11683
11671
  "usehooks-ts",
11684
11672
  "vaul"
@@ -12021,6 +12009,7 @@ import { authClient } from '@cms/auth/client'
12021
12009
  import { Button } from '@cms/components/ui/button'
12022
12010
  import { Input } from '@cms/components/ui/input'
12023
12011
  import { Label } from '@cms/components/ui/label'
12012
+ import { LoaderCircle } from 'lucide-react'
12024
12013
  import { useRouter } from 'next/navigation'
12025
12014
  import * as React from 'react'
12026
12015
 
@@ -12093,6 +12082,7 @@ export function LoginForm() {
12093
12082
  </div>
12094
12083
 
12095
12084
  <Button type="submit" className="w-full" size="lg" disabled={isLoading}>
12085
+ {isLoading && <LoaderCircle className="animate-spin" />}
12096
12086
  {isLoading ? 'Signing in...' : 'Sign In'}
12097
12087
  </Button>
12098
12088
  </form>
@@ -13513,9 +13503,32 @@ const auth = betterAuth({
13513
13503
  const EMAIL = process.env.SEED_EMAIL!
13514
13504
  const PASSWORD = process.env.SEED_PASSWORD!
13515
13505
  const NAME = process.env.SEED_NAME || 'Admin'
13506
+ const OVERWRITE = process.env.SEED_OVERWRITE === 'true'
13516
13507
 
13517
13508
  async function main() {
13518
- console.log('\\n Creating admin user...')
13509
+ // Check if user already exists
13510
+ const existing = await db
13511
+ .select({ id: schema.user.id, name: schema.user.name })
13512
+ .from(schema.user)
13513
+ .where(eq(schema.user.email, EMAIL))
13514
+ .then((rows: { id: string; name: string }[]) => rows[0])
13515
+
13516
+ if (existing && !OVERWRITE) {
13517
+ // Exit code 2 signals "user exists" to the CLI
13518
+ console.log(\`EXISTING_USER:\${existing.name}\`)
13519
+ process.exit(2)
13520
+ }
13521
+
13522
+ if (existing && OVERWRITE) {
13523
+ console.log('\\n Replacing existing admin user...')
13524
+ // Remove existing account + session rows first (foreign key refs)
13525
+ await db.delete(schema.session).where(eq(schema.session.userId, existing.id))
13526
+ await db.delete(schema.account).where(eq(schema.account.userId, existing.id))
13527
+ await db.delete(schema.user).where(eq(schema.user.id, existing.id))
13528
+ } else {
13529
+ console.log('\\n Creating admin user...')
13530
+ }
13531
+
13519
13532
  console.log(\` Email: \${EMAIL}\\n\`)
13520
13533
 
13521
13534
  const result = await auth.api.signUpEmail({
@@ -13532,7 +13545,7 @@ async function main() {
13532
13545
  .set({ role: 'admin' })
13533
13546
  .where(eq(schema.user.id, result.user.id))
13534
13547
 
13535
- console.log(\` Admin user created: \${EMAIL}\`)
13548
+ console.log(\` Admin user \${existing ? 'replaced' : 'created'}: \${EMAIL}\`)
13536
13549
  console.log(' Role: admin\\n')
13537
13550
  process.exit(0)
13538
13551
  }
@@ -13590,24 +13603,54 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13590
13603
  fs31.mkdirSync(scriptsDir, { recursive: true });
13591
13604
  }
13592
13605
  fs31.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13593
- const spinner4 = clack.spinner();
13594
- spinner4.start("Creating admin user...");
13595
- try {
13596
- const { execFileSync: execFileSync5 } = await import("child_process");
13597
- const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
13598
- execFileSync5(tsxBin, [seedPath], {
13606
+ const { execFile } = await import("child_process");
13607
+ const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
13608
+ const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
13609
+ execFile(tsxBin, [seedPath], {
13599
13610
  cwd,
13600
- stdio: "pipe",
13601
13611
  env: {
13602
13612
  ...process.env,
13603
13613
  SEED_EMAIL: email,
13604
13614
  SEED_PASSWORD: password3,
13605
- SEED_NAME: name || "Admin"
13615
+ SEED_NAME: name || "Admin",
13616
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13617
+ }
13618
+ }, (err, stdout, stderr) => {
13619
+ if (err && "code" in err && err.code === 2) {
13620
+ resolve({ code: 2, stdout });
13621
+ } else if (err) {
13622
+ reject(new Error(stderr || err.message));
13623
+ } else {
13624
+ resolve({ code: 0, stdout });
13606
13625
  }
13607
13626
  });
13608
- spinner4.stop("Admin user created");
13627
+ });
13628
+ const spinner5 = clack.spinner();
13629
+ spinner5.start("Creating admin user...");
13630
+ try {
13631
+ const result = await runSeed2(false);
13632
+ if (result.code === 2) {
13633
+ const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
13634
+ spinner5.stop(`Account already exists for ${email}`);
13635
+ const overwrite = await clack.confirm({
13636
+ message: `An admin account (${existingName}) already exists with this email. Replace it?`
13637
+ });
13638
+ if (clack.isCancel(overwrite) || !overwrite) {
13639
+ clack.cancel("Seed cancelled.");
13640
+ try {
13641
+ fs31.unlinkSync(seedPath);
13642
+ } catch {
13643
+ }
13644
+ process.exit(0);
13645
+ }
13646
+ spinner5.start("Replacing admin user...");
13647
+ await runSeed2(true);
13648
+ spinner5.stop("Admin user replaced");
13649
+ } else {
13650
+ spinner5.stop("Admin user created");
13651
+ }
13609
13652
  } catch (err) {
13610
- spinner4.stop("Failed to create admin user");
13653
+ spinner5.stop("Failed to create admin user");
13611
13654
  const errMsg = err instanceof Error ? err.message : String(err);
13612
13655
  clack.log.error(errMsg);
13613
13656
  clack.log.info("You can run the seed script manually:");
@@ -13640,21 +13683,19 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13640
13683
  let isFreshProject = false;
13641
13684
  let srcDir;
13642
13685
  if (project.isExisting) {
13643
- p4.log.info(`Existing Next.js project detected`);
13644
- p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
13686
+ p4.log.info(`Next.js project detected ${pc2.dim("\xB7")} ${pc2.cyan(pm)}`);
13645
13687
  srcDir = project.hasSrcDir;
13646
13688
  if (!project.hasTypeScript) {
13647
13689
  p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13648
13690
  process.exit(1);
13649
13691
  }
13650
13692
  if (project.conflicts.length > 0) {
13651
- p4.log.error("Conflicts detected:");
13652
- for (const conflict of project.conflicts) {
13653
- p4.log.warning(` - ${conflict}`);
13654
- }
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"));
13655
13696
  if (!options.yes) {
13656
13697
  const proceed = await p4.confirm({
13657
- message: "Continue anyway? (existing files will NOT be overwritten)",
13698
+ message: "Continue anyway?",
13658
13699
  initialValue: true
13659
13700
  });
13660
13701
  if (p4.isCancel(proceed) || !proceed) {
@@ -13763,30 +13804,62 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13763
13804
  ...getDefaultConfig(srcDir),
13764
13805
  features: { email: features.includeEmail }
13765
13806
  };
13807
+ const results = [];
13766
13808
  const s = p4.spinner();
13767
- s.start("Creating CMS directory structure...");
13809
+ s.start("Directory structure");
13768
13810
  const baseFiles = scaffoldBase({ cwd, config });
13769
- s.stop(`Created ${baseFiles.length} files`);
13770
- s.start("Configuring TypeScript path aliases...");
13811
+ results.push({ label: "Directory structure", result: `${baseFiles.length} files` });
13812
+ s.message("TypeScript aliases");
13771
13813
  const tsResult = scaffoldTsconfig(cwd);
13772
- s.stop(`Added ${tsResult.added.length} path aliases`);
13773
- 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");
13774
13819
  const twResult = scaffoldTailwind(cwd, srcDir);
13775
- if (twResult.appended) {
13776
- s.stop(`Updated ${twResult.file}`);
13777
- } else if (twResult.file) {
13778
- s.stop("Tailwind already configured for CMS");
13779
- } else {
13780
- s.stop("No CSS file found (will configure later)");
13781
- }
13782
- 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");
13783
13825
  const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
13784
- const envParts = [`Added ${envResult.added.length}`];
13785
- if (envResult.updated.length > 0) envParts.push(`updated ${envResult.updated.length}`);
13786
- s.stop(`${envParts.join(", ")} env vars in .env.local`);
13787
- 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");
13788
13832
  const dbFiles = scaffoldDatabase({ cwd, config });
13789
- 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");
13790
13863
  const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
13791
13864
  if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
13792
13865
  if (!options.yes) {
@@ -13801,42 +13874,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13801
13874
  }
13802
13875
  }
13803
13876
  }
13804
- s.start("Setting up authentication...");
13805
- const authFiles = scaffoldAuth({ cwd, config });
13806
- s.stop(`Created ${authFiles.length} auth files`);
13807
- s.start("Copying CMS components...");
13808
- const compFiles = scaffoldComponents({ cwd, config });
13809
- s.stop(`Created ${compFiles.length} component files`);
13810
- s.start("Creating CMS pages and layouts...");
13811
- const layoutFiles = scaffoldLayout({ cwd, config });
13812
- s.stop(`Created ${layoutFiles.length} page files`);
13813
- s.start("Creating API routes...");
13814
- const apiFiles = scaffoldApiRoutes({ cwd, config });
13815
- s.stop(`Created ${apiFiles.length} API routes`);
13816
- s.start("Checking for linter...");
13817
- if (project.linter.type === "none") {
13818
- s.stop("No linter found");
13819
- s.start("Setting up Biome linter...");
13820
- const biomeResult = scaffoldBiome(cwd, project.linter);
13821
- if (biomeResult.installed) {
13822
- s.stop("Created biome.json");
13823
- } else {
13824
- s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13825
- }
13826
- } else {
13827
- s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
13828
- }
13829
- s.start("Installing dependencies (this may take a minute)...");
13877
+ s.start("Installing dependencies (this may take a minute)");
13830
13878
  const depsResult = await installDependenciesAsync({
13831
13879
  cwd,
13832
13880
  pm,
13833
13881
  includeEmail: features.includeEmail,
13834
13882
  includeBiome: project.linter.type === "none"
13835
13883
  });
13884
+ let depsInstalled = false;
13836
13885
  if (depsResult.success) {
13837
- s.stop(
13838
- `Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`
13839
- );
13886
+ s.stop("");
13887
+ depsInstalled = true;
13840
13888
  } else {
13841
13889
  s.stop("Failed to install dependencies");
13842
13890
  p4.log.warning(depsResult.error ?? "Unknown error");
@@ -13846,24 +13894,59 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13846
13894
  ${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13847
13895
  );
13848
13896
  }
13849
- 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`);
13850
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
+ }
13851
13931
  if (presetResult.errors.length > 0) {
13852
- 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
+ );
13853
13935
  for (const err of presetResult.errors) {
13854
- p4.log.warning(` ${err}`);
13936
+ installLines.push(` ${pc2.dim(err)}`);
13855
13937
  }
13856
13938
  } else {
13857
- s.stop(
13858
- `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`)}`
13859
13941
  );
13860
13942
  }
13943
+ p4.note(installLines.join("\n"), "Installed");
13861
13944
  let dbPushed = false;
13862
13945
  if (depsResult.success && hasDbUrl(cwd)) {
13863
- s.start("Pushing database schema (drizzle-kit push)...");
13946
+ s.start("Pushing database schema (drizzle-kit push)");
13864
13947
  const pushResult = await runDrizzlePush(cwd);
13865
13948
  if (pushResult.success) {
13866
- s.stop("Database schema pushed");
13949
+ s.stop(`${pc2.green("\u2713")} Database schema pushed`);
13867
13950
  dbPushed = true;
13868
13951
  } else {
13869
13952
  s.stop("Database push failed");
@@ -13875,66 +13958,62 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13875
13958
  let seedPassword;
13876
13959
  let seedSuccess = false;
13877
13960
  if (dbPushed && !options.yes) {
13878
- p4.log.step("Create your admin account");
13879
- const email = await p4.text({
13880
- message: "Admin email",
13881
- placeholder: "admin@example.com",
13882
- validate: (v) => {
13883
- 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
+ }
13884
13983
  }
13885
- });
13886
- if (p4.isCancel(email)) {
13887
- p4.cancel("Setup cancelled.");
13888
- process.exit(0);
13889
- }
13890
- const password3 = await p4.password({
13891
- message: "Admin password",
13892
- validate: (v) => {
13893
- 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;
13894
14000
  }
13895
- });
13896
- if (p4.isCancel(password3)) {
13897
- p4.cancel("Setup cancelled.");
13898
- process.exit(0);
13899
14001
  }
13900
- seedEmail = email;
13901
- seedPassword = password3;
13902
- s.start("Creating admin user...");
13903
- const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
13904
14002
  if (seedResult.success) {
13905
- s.stop("Admin user created");
14003
+ s.stop(`${pc2.green("\u2713")} Admin user created`);
13906
14004
  seedSuccess = true;
13907
- } else {
13908
- s.stop("Failed to create admin user");
13909
- p4.log.warning(seedResult.error ?? "Unknown error");
13910
- p4.log.info(`You can run it manually: ${pc2.cyan("npx betterstart seed")}`);
13911
- }
13912
- }
13913
- s.start("Generating documentation...");
13914
- {
13915
- const entityNames = [];
13916
- const formNames = [];
13917
- const schemasDir = path37.join(cwd, config.paths.schemas);
13918
- const formsDir = path37.join(schemasDir, "forms");
13919
- if (fs32.existsSync(schemasDir)) {
13920
- for (const f of fs32.readdirSync(schemasDir)) {
13921
- if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13922
- }
13923
- }
13924
- if (fs32.existsSync(formsDir)) {
13925
- for (const f of fs32.readdirSync(formsDir)) {
13926
- if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13927
- }
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
+ );
13928
14013
  }
13929
- regenerateCmsDoc(cwd, config, {
13930
- preset: features.preset,
13931
- schemas: entityNames,
13932
- forms: formNames
13933
- });
13934
14014
  }
13935
- s.stop("Generated CMS.md");
13936
14015
  if (isFreshProject) {
13937
- s.start("Creating initial git commit...");
14016
+ s.start("Creating initial git commit");
13938
14017
  try {
13939
14018
  execFileSync4("git", ["init"], { cwd, stdio: "pipe" });
13940
14019
  execFileSync4("git", ["add", "."], { cwd, stdio: "pipe" });
@@ -13950,7 +14029,6 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13950
14029
  const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
13951
14030
  const summaryLines = [
13952
14031
  `Preset: ${pc2.cyan(features.preset)}`,
13953
- `Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
13954
14032
  `Files created: ${pc2.cyan(String(totalFiles))}`,
13955
14033
  `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
13956
14034
  ];
@@ -14040,26 +14118,14 @@ function hasDbUrl(cwd) {
14040
14118
  }
14041
14119
  return false;
14042
14120
  }
14043
- async function runSeed(cwd, cmsDir, email, password3) {
14121
+ function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
14044
14122
  const scriptsDir = path37.join(cwd, cmsDir, "scripts");
14045
14123
  const seedPath = path37.join(scriptsDir, "seed.ts");
14046
14124
  if (!fs32.existsSync(scriptsDir)) {
14047
14125
  fs32.mkdirSync(scriptsDir, { recursive: true });
14048
14126
  }
14049
14127
  fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
14050
- try {
14051
- const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
14052
- execFileSync4(tsxBin, [seedPath], {
14053
- cwd,
14054
- stdio: "pipe",
14055
- timeout: 3e4,
14056
- env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
14057
- });
14058
- return { success: true, error: null };
14059
- } catch (err) {
14060
- const msg = err instanceof Error ? err.message : String(err);
14061
- return { success: false, error: msg };
14062
- } finally {
14128
+ const cleanup = () => {
14063
14129
  try {
14064
14130
  fs32.unlinkSync(seedPath);
14065
14131
  if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
@@ -14067,7 +14133,71 @@ async function runSeed(cwd, cmsDir, email, password3) {
14067
14133
  }
14068
14134
  } catch {
14069
14135
  }
14070
- }
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";
14071
14201
  }
14072
14202
  function runDrizzlePush(cwd) {
14073
14203
  return new Promise((resolve) => {
@@ -14481,44 +14611,6 @@ function findNextNonEmptyLine(lines, startIndex) {
14481
14611
  }
14482
14612
  return null;
14483
14613
  }
14484
- function cleanPackageJsonDeps(pkgPath, allDeps, allDevDeps) {
14485
- if (!fs34.existsSync(pkgPath)) return { removed: [], removedDev: [] };
14486
- const content = fs34.readFileSync(pkgPath, "utf-8");
14487
- let pkg;
14488
- try {
14489
- pkg = JSON.parse(content);
14490
- } catch {
14491
- return { removed: [], removedDev: [] };
14492
- }
14493
- const deps = pkg.dependencies ?? {};
14494
- const devDeps = pkg.devDependencies ?? {};
14495
- const depNames = new Set(allDeps.map((d) => d.split("@").slice(0, d.startsWith("@") ? 2 : 1).join("@")));
14496
- const devDepNames = new Set(
14497
- allDevDeps.map((d) => d.split("@").slice(0, d.startsWith("@") ? 2 : 1).join("@"))
14498
- );
14499
- const removed = [];
14500
- for (const name of Object.keys(deps)) {
14501
- if (depNames.has(name)) {
14502
- delete deps[name];
14503
- removed.push(name);
14504
- }
14505
- }
14506
- const removedDev = [];
14507
- for (const name of Object.keys(devDeps)) {
14508
- if (devDepNames.has(name)) {
14509
- delete devDeps[name];
14510
- removedDev.push(name);
14511
- }
14512
- }
14513
- if (removed.length === 0 && removedDev.length === 0) {
14514
- return { removed: [], removedDev: [] };
14515
- }
14516
- pkg.dependencies = deps;
14517
- pkg.devDependencies = devDeps;
14518
- fs34.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
14519
- `, "utf-8");
14520
- return { removed, removedDev };
14521
- }
14522
14614
 
14523
14615
  // src/commands/uninstall.ts
14524
14616
  function findMainCss2(cwd) {
@@ -14560,6 +14652,8 @@ function buildUninstallPlan(cwd) {
14560
14652
  steps.push({
14561
14653
  label: "CMS directories",
14562
14654
  items: dirs,
14655
+ count: dirs.length,
14656
+ unit: dirs.length === 1 ? "directory" : "directories",
14563
14657
  execute() {
14564
14658
  if (fs35.existsSync(cmsDir)) fs35.rmSync(cmsDir, { recursive: true, force: true });
14565
14659
  if (fs35.existsSync(cmsRouteGroup)) fs35.rmSync(cmsRouteGroup, { recursive: true, force: true });
@@ -14588,6 +14682,8 @@ function buildUninstallPlan(cwd) {
14588
14682
  steps.push({
14589
14683
  label: "Config files",
14590
14684
  items: configFiles,
14685
+ count: configFiles.length,
14686
+ unit: configFiles.length === 1 ? "file" : "files",
14591
14687
  execute() {
14592
14688
  for (const p6 of configPaths) {
14593
14689
  if (fs35.existsSync(p6)) fs35.unlinkSync(p6);
@@ -14598,10 +14694,14 @@ function buildUninstallPlan(cwd) {
14598
14694
  const tsconfigPath = path40.join(cwd, "tsconfig.json");
14599
14695
  if (fs35.existsSync(tsconfigPath)) {
14600
14696
  const content = fs35.readFileSync(tsconfigPath, "utf-8");
14601
- if (content.includes("@cms/")) {
14697
+ const aliasMatches = content.match(/"@cms\//g);
14698
+ if (aliasMatches && aliasMatches.length > 0) {
14699
+ const aliasCount = aliasMatches.length;
14602
14700
  steps.push({
14603
14701
  label: "tsconfig.json path aliases",
14604
- items: ["Remove all @cms/* paths from compilerOptions.paths"],
14702
+ items: [`@cms/* aliases in tsconfig.json`],
14703
+ count: aliasCount,
14704
+ unit: aliasCount === 1 ? "alias" : "aliases",
14605
14705
  execute() {
14606
14706
  cleanTsconfig(tsconfigPath);
14607
14707
  }
@@ -14611,12 +14711,14 @@ function buildUninstallPlan(cwd) {
14611
14711
  const cssFile = findMainCss2(cwd);
14612
14712
  if (cssFile) {
14613
14713
  const cssContent = fs35.readFileSync(cssFile, "utf-8");
14614
- const sourcePattern = /^@source\s+"[^"]*cms[^"]*";\s*$/m;
14615
- if (sourcePattern.test(cssContent)) {
14714
+ const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14715
+ if (sourceLines.length > 0) {
14616
14716
  const relCss = path40.relative(cwd, cssFile);
14617
14717
  steps.push({
14618
14718
  label: `CSS @source lines (${relCss})`,
14619
- items: ["Remove @source lines referencing cms/"],
14719
+ items: [`@source lines in ${relCss}`],
14720
+ count: sourceLines.length,
14721
+ unit: sourceLines.length === 1 ? "line" : "lines",
14620
14722
  execute() {
14621
14723
  cleanCss(cssFile);
14622
14724
  }
@@ -14630,39 +14732,15 @@ function buildUninstallPlan(cwd) {
14630
14732
  if (bsVars.length > 0) {
14631
14733
  steps.push({
14632
14734
  label: ".env.local variables",
14633
- items: bsVars,
14735
+ items: ["BETTERSTART_* vars in .env.local"],
14736
+ count: bsVars.length,
14737
+ unit: bsVars.length === 1 ? "variable" : "variables",
14634
14738
  execute() {
14635
14739
  cleanEnvFile(envPath);
14636
14740
  }
14637
14741
  });
14638
14742
  }
14639
14743
  }
14640
- const pkgPath = path40.join(cwd, "package.json");
14641
- if (fs35.existsSync(pkgPath)) {
14642
- const allCoreDeps = [...CORE_DEPS, ...EMAIL_DEPS];
14643
- const allDevDeps = [...DEV_DEPS, ...BIOME_DEV_DEPS];
14644
- const coreNames = new Set(
14645
- allCoreDeps.map((d) => d.split("@").slice(0, d.startsWith("@") ? 2 : 1).join("@"))
14646
- );
14647
- const devNames = new Set(
14648
- allDevDeps.map((d) => d.split("@").slice(0, d.startsWith("@") ? 2 : 1).join("@"))
14649
- );
14650
- const pkgContent = JSON.parse(fs35.readFileSync(pkgPath, "utf-8"));
14651
- const deps = Object.keys(pkgContent.dependencies ?? {}).filter((n) => coreNames.has(n));
14652
- const devDeps = Object.keys(pkgContent.devDependencies ?? {}).filter((n) => devNames.has(n));
14653
- if (deps.length > 0 || devDeps.length > 0) {
14654
- const items = [];
14655
- if (deps.length > 0) items.push(`${deps.length} dependencies`);
14656
- if (devDeps.length > 0) items.push(`${devDeps.length} devDependencies`);
14657
- steps.push({
14658
- label: "package.json dependencies",
14659
- items,
14660
- execute() {
14661
- cleanPackageJsonDeps(pkgPath, allCoreDeps, allDevDeps);
14662
- }
14663
- });
14664
- }
14665
- }
14666
14744
  return steps;
14667
14745
  }
14668
14746
  var uninstallCommand = new Command6("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
@@ -14670,60 +14748,39 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14670
14748
  p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14671
14749
  const steps = buildUninstallPlan(cwd);
14672
14750
  if (steps.length === 0) {
14673
- 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.");
14674
14752
  p5.outro("Done");
14675
14753
  return;
14676
14754
  }
14677
- p5.log.warn(
14678
- `Found ${steps.length} ${steps.length === 1 ? "area" : "areas"} to clean up:`
14679
- );
14680
- let completedCount = 0;
14681
- for (const step of steps) {
14682
- p5.log.message("");
14683
- p5.log.step(pc3.bold(step.label));
14684
- for (const item of step.items) {
14685
- p5.log.message(` ${pc3.dim("\u2022")} ${item}`);
14686
- }
14687
- if (!options.force) {
14688
- const confirmed = await p5.confirm({
14689
- message: `Remove ${step.label}?`,
14690
- initialValue: true
14691
- });
14692
- if (p5.isCancel(confirmed)) {
14693
- p5.cancel("Uninstall cancelled.");
14694
- process.exit(0);
14695
- }
14696
- if (!confirmed) {
14697
- p5.log.info(pc3.dim(`Skipped: ${step.label}`));
14698
- continue;
14699
- }
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");
14761
+ if (!options.force) {
14762
+ const confirmed = await p5.confirm({
14763
+ message: "Proceed with uninstall?",
14764
+ initialValue: false
14765
+ });
14766
+ if (p5.isCancel(confirmed) || !confirmed) {
14767
+ p5.cancel("Uninstall cancelled.");
14768
+ process.exit(0);
14700
14769
  }
14701
- step.execute();
14702
- completedCount++;
14703
- p5.log.success(`Removed: ${step.label}`);
14704
- }
14705
- p5.log.message("");
14706
- if (completedCount === 0) {
14707
- p5.log.info("No changes were made.");
14708
- } else {
14709
- const pm = detectPackageManager(cwd);
14710
- p5.note(
14711
- [
14712
- `Run ${pc3.cyan(installCommand(pm))} to clean up node_modules.`,
14713
- "",
14714
- pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed.")
14715
- ].join("\n"),
14716
- "Next steps"
14717
- );
14718
14770
  }
14719
- if (findMainCss2(cwd)) {
14720
- p5.log.info(
14721
- pc3.dim(
14722
- "Note: @theme tokens were left in your CSS \u2014 they're harmless and may be shared with your own styles."
14723
- )
14724
- );
14771
+ const s = p5.spinner();
14772
+ s.start(steps[0].label);
14773
+ for (const step of steps) {
14774
+ s.message(step.label);
14775
+ step.execute();
14725
14776
  }
14726
- 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");
14727
14784
  });
14728
14785
 
14729
14786
  // src/commands/update-styles.ts