@betterstart/cli 0.1.6 → 0.1.8

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
@@ -897,8 +897,8 @@ function toPascalCase3(str) {
897
897
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
898
898
  }
899
899
  function toCamelCase(str) {
900
- const p4 = toPascalCase3(str);
901
- return p4.charAt(0).toLowerCase() + p4.slice(1);
900
+ const p5 = toPascalCase3(str);
901
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
902
902
  }
903
903
  function toKebabCase(str) {
904
904
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -989,7 +989,7 @@ function generateFormAdminPages(schema, cwd, pagesDir, options) {
989
989
  const adminDir = path4.join(cwd, pagesDir, "forms", kebab);
990
990
  if (!fs4.existsSync(adminDir)) fs4.mkdirSync(adminDir, { recursive: true });
991
991
  const files = [];
992
- const rel = (p4) => path4.relative(cwd, p4);
992
+ const rel = (p5) => path4.relative(cwd, p5);
993
993
  const pagePath = path4.join(adminDir, "page.tsx");
994
994
  if (!fs4.existsSync(pagePath) || options.force) {
995
995
  fs4.writeFileSync(pagePath, generatePage(pascal, kebab), "utf-8");
@@ -1812,9 +1812,26 @@ import path5 from "path";
1812
1812
  function parseNavigationFile(content) {
1813
1813
  const iconImportMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
1814
1814
  const iconImports = iconImportMatch ? iconImportMatch[1].split(",").map((s) => s.trim()).filter((s) => s && s !== "LucideIcon") : [];
1815
- const match = content.match(/export const cmsNavigation[^=]*=\s*\[([\s\S]*?)\]\s*(?:as const)?/);
1816
- if (!match) return { items: [], iconImports };
1817
- return { items: parseItemsBlock(match[1]), iconImports };
1815
+ const arrayBlock = extractTopLevelArray(content);
1816
+ if (!arrayBlock) return { items: [], iconImports };
1817
+ return { items: parseItemsBlock(arrayBlock), iconImports };
1818
+ }
1819
+ function extractTopLevelArray(content) {
1820
+ const marker = content.indexOf("cmsNavigation");
1821
+ if (marker === -1) return null;
1822
+ const eqSign = content.indexOf("=", marker);
1823
+ if (eqSign === -1) return null;
1824
+ const openBracket = content.indexOf("[", eqSign);
1825
+ if (openBracket === -1) return null;
1826
+ let depth = 0;
1827
+ for (let i = openBracket; i < content.length; i++) {
1828
+ if (content[i] === "[") depth++;
1829
+ if (content[i] === "]") depth--;
1830
+ if (depth === 0) {
1831
+ return content.slice(openBracket + 1, i);
1832
+ }
1833
+ }
1834
+ return null;
1818
1835
  }
1819
1836
  function parseItemsBlock(block) {
1820
1837
  const items = [];
@@ -1940,20 +1957,9 @@ function updateFormNavigation(schema, cwd, cmsDir, options = {}) {
1940
1957
  formsGroup.children.sort((a, b) => a.label.localeCompare(b.label));
1941
1958
  }
1942
1959
  const dashboard = items.find((item) => item.href === "/cms");
1943
- const forms = items.find((item) => item.label === "Forms");
1944
- const users = items.find((item) => item.label === "Users");
1945
- const settings = items.find((item) => item.label === "Settings");
1946
- const others = items.filter(
1947
- (item) => item.href !== "/cms" && item.label !== "Forms" && item.label !== "Users" && item.label !== "Settings"
1948
- );
1960
+ const others = items.filter((item) => item.href !== "/cms");
1949
1961
  others.sort((a, b) => a.label.localeCompare(b.label));
1950
- items = [
1951
- ...dashboard ? [dashboard] : [],
1952
- ...others,
1953
- ...forms ? [forms] : [],
1954
- ...users ? [users] : [],
1955
- ...settings ? [settings] : []
1956
- ];
1962
+ items = [...dashboard ? [dashboard] : [], ...others];
1957
1963
  for (const icon of ["FileInput", "Inbox"]) {
1958
1964
  if (!iconImports.includes(icon)) {
1959
1965
  iconImports.push(icon);
@@ -1971,8 +1977,8 @@ function toPascalCase4(str) {
1971
1977
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
1972
1978
  }
1973
1979
  function toCamelCase2(str) {
1974
- const p4 = toPascalCase4(str);
1975
- return p4.charAt(0).toLowerCase() + p4.slice(1);
1980
+ const p5 = toPascalCase4(str);
1981
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
1976
1982
  }
1977
1983
  function toKebabCase3(str) {
1978
1984
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -2687,8 +2693,8 @@ function toPascalCase5(str) {
2687
2693
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
2688
2694
  }
2689
2695
  function toCamelCase3(str) {
2690
- const p4 = toPascalCase5(str);
2691
- return p4.charAt(0).toLowerCase() + p4.slice(1);
2696
+ const p5 = toPascalCase5(str);
2697
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
2692
2698
  }
2693
2699
  function singularize2(str) {
2694
2700
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -3074,7 +3080,7 @@ ${autoSlugUpdate}
3074
3080
  for (const [key, value] of Object.entries(updateData)) {
3075
3081
  if (key === 'published') { processedData[key] = value; continue }
3076
3082
  const field = fieldMeta.find(f => f.name === key)
3077
- if (!field) { processedData[key] = value; continue }
3083
+ if (!field) continue
3078
3084
  if (field.type === 'list') {
3079
3085
  processedData[key] = value || []
3080
3086
  } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select'].includes(field.type)) {
@@ -3251,7 +3257,7 @@ export async function upsert${Singular}(input: Upsert${Singular}Input): Promise<
3251
3257
  const processedData: Record<string, unknown> = {}
3252
3258
  for (const [key, value] of Object.entries(input)) {
3253
3259
  const field = fieldMeta.find(f => f.name === key)
3254
- if (!field) { processedData[key] = value; continue }
3260
+ if (!field) continue
3255
3261
  if (field.type === 'list') {
3256
3262
  processedData[key] = value || []
3257
3263
  } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select'].includes(field.type)) {
@@ -3411,8 +3417,8 @@ function toPascalCase6(str) {
3411
3417
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
3412
3418
  }
3413
3419
  function toCamelCase4(str) {
3414
- const p4 = toPascalCase6(str);
3415
- return p4.charAt(0).toLowerCase() + p4.slice(1);
3420
+ const p5 = toPascalCase6(str);
3421
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
3416
3422
  }
3417
3423
  function singularize3(str) {
3418
3424
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -4554,8 +4560,8 @@ function toPascalCase9(str) {
4554
4560
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4555
4561
  }
4556
4562
  function toCamelCase5(str) {
4557
- const p4 = toPascalCase9(str);
4558
- return p4.charAt(0).toLowerCase() + p4.slice(1);
4563
+ const p5 = toPascalCase9(str);
4564
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
4559
4565
  }
4560
4566
  function singularize6(str) {
4561
4567
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -4787,8 +4793,8 @@ function toPascalCase10(str) {
4787
4793
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4788
4794
  }
4789
4795
  function toCamelCase6(str) {
4790
- const p4 = toPascalCase10(str);
4791
- return p4.charAt(0).toLowerCase() + p4.slice(1);
4796
+ const p5 = toPascalCase10(str);
4797
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
4792
4798
  }
4793
4799
  function singularize7(str) {
4794
4800
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -5039,6 +5045,7 @@ function generateFieldJSXCore(field, indent = " ") {
5039
5045
  const fieldType = getFormFieldType(field);
5040
5046
  const label = field.label || field.name;
5041
5047
  const hintJSX = field.hint ? `${indent} <FormDescription>${field.hint}</FormDescription>` : "";
5048
+ const listHintJSX = field.hint ? `${indent} <p className="text-[0.8rem] text-muted-foreground">${field.hint}</p>` : "";
5042
5049
  if (field.type === "group" && field.fields) {
5043
5050
  const columns = field.columns || 1;
5044
5051
  const gridClass = columns > 1 ? `grid-cols-${columns}` : "grid-cols-1";
@@ -5498,7 +5505,7 @@ ${indent} )}
5498
5505
  ${indent} {${field.name}FieldArray.fields.length > 0 && (
5499
5506
  ${indent} <div className="space-y-5">
5500
5507
  ${indent} <div className="flex items-center justify-between">
5501
- ${indent} <FormLabel className="text-base">${label}</FormLabel>
5508
+ ${indent} <Label className="text-base">${label}</Label>
5502
5509
  ${indent} <Button
5503
5510
  ${indent} type="button"
5504
5511
  ${indent} variant="outline"
@@ -5513,7 +5520,7 @@ ${indent} <Plus className="size-3" />
5513
5520
  ${indent} Add ${singularLabel}
5514
5521
  ${indent} </Button>
5515
5522
  ${indent} </div>
5516
- ${hintJSX ? `${hintJSX}
5523
+ ${listHintJSX ? `${listHintJSX}
5517
5524
  ` : ""}${indent} <Accordion
5518
5525
  ${indent} type="single"
5519
5526
  ${indent} collapsible
@@ -5807,6 +5814,7 @@ ${tabsContent}
5807
5814
  AccordionTrigger
5808
5815
  } from '@cms/components/ui/accordion'`);
5809
5816
  if (!hasSeparator) uiImports.push("import { Separator } from '@cms/components/ui/separator'");
5817
+ uiImports.push("import { Label } from '@cms/components/ui/label'");
5810
5818
  }
5811
5819
  const lucideIcons = [];
5812
5820
  if (hasRelationship) lucideIcons.push("Check", "ChevronsUpDown");
@@ -6134,6 +6142,7 @@ ${tabsContent}
6134
6142
  AccordionTrigger
6135
6143
  } from '@cms/components/ui/accordion'`);
6136
6144
  if (!hasSeparator) uiImports.push("import { Separator } from '@cms/components/ui/separator'");
6145
+ uiImports.push("import { Label } from '@cms/components/ui/label'");
6137
6146
  }
6138
6147
  const lucideIcons = [];
6139
6148
  if (hasRelationship) lucideIcons.push("Check", "ChevronsUpDown");
@@ -6406,11 +6415,28 @@ import path15 from "path";
6406
6415
  function parseNavigationFile2(content) {
6407
6416
  const iconImportMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
6408
6417
  const iconImports = iconImportMatch ? iconImportMatch[1].split(",").map((s) => s.trim()).filter((s) => s && s !== "LucideIcon") : [];
6409
- const match = content.match(/export const cmsNavigation[^=]*=\s*\[([\s\S]*?)\]\s*(?:as const)?/);
6410
- if (!match) return { items: [], iconImports };
6411
- const items = parseItemsBlock2(match[1]);
6418
+ const arrayBlock = extractTopLevelArray2(content);
6419
+ if (!arrayBlock) return { items: [], iconImports };
6420
+ const items = parseItemsBlock2(arrayBlock);
6412
6421
  return { items, iconImports };
6413
6422
  }
6423
+ function extractTopLevelArray2(content) {
6424
+ const marker = content.indexOf("cmsNavigation");
6425
+ if (marker === -1) return null;
6426
+ const eqSign = content.indexOf("=", marker);
6427
+ if (eqSign === -1) return null;
6428
+ const openBracket = content.indexOf("[", eqSign);
6429
+ if (openBracket === -1) return null;
6430
+ let depth = 0;
6431
+ for (let i = openBracket; i < content.length; i++) {
6432
+ if (content[i] === "[") depth++;
6433
+ if (content[i] === "]") depth--;
6434
+ if (depth === 0) {
6435
+ return content.slice(openBracket + 1, i);
6436
+ }
6437
+ }
6438
+ return null;
6439
+ }
6414
6440
  function parseItemsBlock2(block) {
6415
6441
  const items = [];
6416
6442
  let depth = 0;
@@ -6497,6 +6523,9 @@ function appendItem2(lines, item, indent, isLast) {
6497
6523
  }
6498
6524
  function updateNavigation(schema, cwd, cmsDir, options = {}) {
6499
6525
  const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
6526
+ if (schema.name === "settings") {
6527
+ return { files: [] };
6528
+ }
6500
6529
  let items = [];
6501
6530
  let iconImports = [];
6502
6531
  if (fs15.existsSync(navFilePath)) {
@@ -6506,38 +6535,59 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
6506
6535
  iconImports = parsed.iconImports;
6507
6536
  }
6508
6537
  const entityHref = `/cms/${schema.name}`;
6509
- const existingIndex = items.findIndex((item) => item.href === entityHref);
6510
6538
  const newItem = {
6511
6539
  label: schema.label,
6512
6540
  href: entityHref,
6513
6541
  icon: schema.icon
6514
6542
  };
6515
- if (existingIndex >= 0) {
6516
- if (options.force) {
6517
- items[existingIndex] = newItem;
6543
+ if (schema.navGroup) {
6544
+ let group = items.find((item) => item.label === schema.navGroup?.label);
6545
+ if (!group) {
6546
+ group = {
6547
+ label: schema.navGroup.label,
6548
+ href: "#",
6549
+ icon: schema.navGroup.icon,
6550
+ children: []
6551
+ };
6552
+ items.push(group);
6553
+ }
6554
+ if (!group.children) {
6555
+ group.children = [];
6556
+ }
6557
+ const existingChild = group.children.findIndex((c) => c.href === entityHref);
6558
+ if (existingChild >= 0) {
6559
+ if (options.force) {
6560
+ group.children[existingChild] = newItem;
6561
+ } else {
6562
+ return { files: [] };
6563
+ }
6518
6564
  } else {
6519
- return { files: [] };
6565
+ group.children.push(newItem);
6566
+ group.children.sort((a, b) => a.label.localeCompare(b.label));
6567
+ }
6568
+ if (schema.navGroup.icon && !iconImports.includes(schema.navGroup.icon)) {
6569
+ iconImports.push(schema.navGroup.icon);
6520
6570
  }
6521
6571
  } else {
6522
- const dashboard = items.find((item) => item.href === "/cms");
6523
- const users = items.find((item) => item.label === "Users");
6524
- const settings = items.find((item) => item.label === "Settings");
6525
- const others = items.filter(
6526
- (item) => item.href !== "/cms" && item.label !== "Users" && item.label !== "Settings"
6527
- );
6528
- others.push(newItem);
6529
- others.sort((a, b) => a.label.localeCompare(b.label));
6530
- items = [
6531
- ...dashboard ? [dashboard] : [],
6532
- ...others,
6533
- ...users ? [users] : [],
6534
- ...settings ? [settings] : []
6535
- ];
6572
+ const existingIndex = items.findIndex((item) => item.href === entityHref);
6573
+ if (existingIndex >= 0) {
6574
+ if (options.force) {
6575
+ items[existingIndex] = newItem;
6576
+ } else {
6577
+ return { files: [] };
6578
+ }
6579
+ } else {
6580
+ items.push(newItem);
6581
+ }
6536
6582
  }
6583
+ const dashboard = items.find((item) => item.href === "/cms");
6584
+ const others = items.filter((item) => item.href !== "/cms");
6585
+ others.sort((a, b) => a.label.localeCompare(b.label));
6586
+ items = [...dashboard ? [dashboard] : [], ...others];
6537
6587
  if (schema.icon && !iconImports.includes(schema.icon)) {
6538
6588
  iconImports.push(schema.icon);
6539
- iconImports.sort();
6540
6589
  }
6590
+ iconImports.sort();
6541
6591
  const dir = path15.dirname(navFilePath);
6542
6592
  if (!fs15.existsSync(dir)) {
6543
6593
  fs15.mkdirSync(dir, { recursive: true });
@@ -6770,7 +6820,7 @@ ${filterLogic}` : ""}`;
6770
6820
  className="w-[200px] justify-between"
6771
6821
  >
6772
6822
  {${f.field} || '${f.label}'}
6773
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
6823
+ <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
6774
6824
  </Button>
6775
6825
  </PopoverTrigger>
6776
6826
  <PopoverContent className="w-[200px] p-0">
@@ -6791,7 +6841,7 @@ ${filterLogic}` : ""}`;
6791
6841
  >
6792
6842
  <Check
6793
6843
  className={cn(
6794
- 'mr-2 h-4 w-4',
6844
+ 'mr-2 size-4',
6795
6845
  ${f.field} === '' ? 'opacity-100' : 'opacity-0'
6796
6846
  )}
6797
6847
  />
@@ -6810,7 +6860,7 @@ ${filterLogic}` : ""}`;
6810
6860
  >
6811
6861
  <Check
6812
6862
  className={cn(
6813
- 'mr-2 h-4 w-4',
6863
+ 'mr-2 size-4',
6814
6864
  ${f.field} === option ? 'opacity-100' : 'opacity-0'
6815
6865
  )}
6816
6866
  />
@@ -6973,8 +7023,8 @@ function toPascalCase16(str) {
6973
7023
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
6974
7024
  }
6975
7025
  function toCamelCase7(str) {
6976
- const p4 = toPascalCase16(str);
6977
- return p4.charAt(0).toLowerCase() + p4.slice(1);
7026
+ const p5 = toPascalCase16(str);
7027
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
6978
7028
  }
6979
7029
  function singularize12(str) {
6980
7030
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -7568,6 +7618,18 @@ function detectPackageManager(cwd) {
7568
7618
  }
7569
7619
  return "npm";
7570
7620
  }
7621
+ function runCommand(pm, script) {
7622
+ switch (pm) {
7623
+ case "pnpm":
7624
+ return `pnpm ${script}`;
7625
+ case "yarn":
7626
+ return `yarn ${script}`;
7627
+ case "bun":
7628
+ return `bun run ${script}`;
7629
+ default:
7630
+ return `npm run ${script}`;
7631
+ }
7632
+ }
7571
7633
 
7572
7634
  // src/generators/post-generate.ts
7573
7635
  function loadEnvFile(cwd) {
@@ -7786,29 +7848,107 @@ var generateCommand = new Command("generate").alias("g").description("Generate e
7786
7848
  );
7787
7849
 
7788
7850
  // src/commands/init.ts
7789
- import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
7790
- import fs31 from "fs";
7791
- import path36 from "path";
7792
- import * as p3 from "@clack/prompts";
7793
- import { Command as Command2 } from "commander";
7851
+ import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
7852
+ import fs32 from "fs";
7853
+ import path37 from "path";
7854
+ import * as p4 from "@clack/prompts";
7855
+ import { Command as Command3 } from "commander";
7856
+ import pc2 from "picocolors";
7857
+
7858
+ // src/init/prompts/database.ts
7859
+ import { execFileSync as execFileSync3 } from "child_process";
7860
+ import * as p from "@clack/prompts";
7794
7861
  import pc from "picocolors";
7862
+ var VERCEL_NEON_URL = "https://vercel.com/dashboard/integrations/checkout/neon";
7863
+ async function promptDatabase() {
7864
+ while (true) {
7865
+ const choice = await p.select({
7866
+ message: "How would you like to connect your database?",
7867
+ options: [
7868
+ {
7869
+ value: "vercel-neon",
7870
+ label: "Vercel (Neon)",
7871
+ hint: "opens browser to create a free Postgres database"
7872
+ },
7873
+ {
7874
+ value: "supabase",
7875
+ label: "Supabase",
7876
+ hint: "coming soon"
7877
+ },
7878
+ {
7879
+ value: "manual",
7880
+ label: "Enter connection string manually"
7881
+ }
7882
+ ]
7883
+ });
7884
+ if (p.isCancel(choice)) {
7885
+ p.cancel("Setup cancelled.");
7886
+ process.exit(0);
7887
+ }
7888
+ if (choice === "supabase") {
7889
+ p.log.warning("Supabase support is coming soon. Please choose another option.");
7890
+ continue;
7891
+ }
7892
+ if (choice === "vercel-neon") {
7893
+ openBrowser(VERCEL_NEON_URL);
7894
+ p.log.info(
7895
+ `Opening Vercel\u2026 Create a Neon Postgres database, then copy the ${pc.cyan("DATABASE_URL")} from the dashboard.`
7896
+ );
7897
+ }
7898
+ const url = await promptConnectionString();
7899
+ return { url };
7900
+ }
7901
+ }
7902
+ async function promptConnectionString() {
7903
+ const input = await p.text({
7904
+ message: "Paste your database connection string",
7905
+ placeholder: "postgres://user:pass@host/db",
7906
+ validate(val) {
7907
+ if (!val.trim()) {
7908
+ return "A connection string is required to continue";
7909
+ }
7910
+ const stripped = val.replace(/^['"]|['"]$/g, "");
7911
+ if (!stripped.startsWith("postgres://") && !stripped.startsWith("postgresql://")) {
7912
+ return "Must start with postgres:// or postgresql://";
7913
+ }
7914
+ }
7915
+ });
7916
+ if (p.isCancel(input)) {
7917
+ p.cancel("Setup cancelled.");
7918
+ process.exit(0);
7919
+ }
7920
+ return input.replace(/^['"]|['"]$/g, "").trim();
7921
+ }
7922
+ function openBrowser(url) {
7923
+ try {
7924
+ const platform = process.platform;
7925
+ if (platform === "darwin") {
7926
+ execFileSync3("open", [url], { stdio: "ignore" });
7927
+ } else if (platform === "win32") {
7928
+ execFileSync3("cmd", ["/c", "start", url], { stdio: "ignore" });
7929
+ } else {
7930
+ execFileSync3("xdg-open", [url], { stdio: "ignore" });
7931
+ }
7932
+ } catch {
7933
+ }
7934
+ }
7795
7935
 
7796
7936
  // src/init/prompts/features.ts
7797
- import * as p from "@clack/prompts";
7937
+ import * as p2 from "@clack/prompts";
7798
7938
  async function promptFeatures(presetOverride) {
7799
- const includeEmail = await p.confirm({
7939
+ const includeEmail = await p2.confirm({
7800
7940
  message: "Include email system? (Resend + React Email)",
7801
7941
  initialValue: true
7802
7942
  });
7803
- if (p.isCancel(includeEmail)) {
7804
- p.cancel("Setup cancelled.");
7943
+ if (p2.isCancel(includeEmail)) {
7944
+ p2.cancel("Setup cancelled.");
7805
7945
  process.exit(0);
7806
7946
  }
7807
7947
  let preset;
7808
7948
  if (presetOverride && isValidPreset(presetOverride)) {
7809
7949
  preset = presetOverride;
7810
7950
  } else {
7811
- const selected = await p.select({
7951
+ const selected = await p2.select({
7812
7952
  message: "Select a preset:",
7813
7953
  options: [
7814
7954
  { value: "blog", label: "Blog", hint: "Posts + Categories (recommended)" },
@@ -7817,8 +7957,8 @@ async function promptFeatures(presetOverride) {
7817
7957
  ],
7818
7958
  initialValue: "blog"
7819
7959
  });
7820
- if (p.isCancel(selected)) {
7821
- p.cancel("Setup cancelled.");
7960
+ if (p2.isCancel(selected)) {
7961
+ p2.cancel("Setup cancelled.");
7822
7962
  process.exit(0);
7823
7963
  }
7824
7964
  preset = selected;
@@ -7830,9 +7970,9 @@ function isValidPreset(value) {
7830
7970
  }
7831
7971
 
7832
7972
  // src/init/prompts/project.ts
7833
- import * as p2 from "@clack/prompts";
7973
+ import * as p3 from "@clack/prompts";
7834
7974
  async function promptProject(defaultName) {
7835
- const projectName = await p2.text({
7975
+ const projectName = await p3.text({
7836
7976
  message: "What is your project name?",
7837
7977
  placeholder: defaultName ?? "my-app",
7838
7978
  defaultValue: defaultName ?? "my-app",
@@ -7844,16 +7984,16 @@ async function promptProject(defaultName) {
7844
7984
  return void 0;
7845
7985
  }
7846
7986
  });
7847
- if (p2.isCancel(projectName)) {
7848
- p2.cancel("Setup cancelled.");
7987
+ if (p3.isCancel(projectName)) {
7988
+ p3.cancel("Setup cancelled.");
7849
7989
  process.exit(0);
7850
7990
  }
7851
- const useSrcDir = await p2.confirm({
7991
+ const useSrcDir = await p3.confirm({
7852
7992
  message: "Use src/ directory?",
7853
7993
  initialValue: true
7854
7994
  });
7855
- if (p2.isCancel(useSrcDir)) {
7856
- p2.cancel("Setup cancelled.");
7995
+ if (p3.isCancel(useSrcDir)) {
7996
+ p3.cancel("Setup cancelled.");
7857
7997
  process.exit(0);
7858
7998
  }
7859
7999
  return { projectName: projectName.trim(), useSrcDir };
@@ -8390,8 +8530,139 @@ function scaffoldBiome(cwd, linter) {
8390
8530
  }
8391
8531
 
8392
8532
  // src/init/scaffolders/components.ts
8533
+ import path29 from "path";
8534
+ import fs26 from "fs-extra";
8535
+
8536
+ // src/utils/detect.ts
8537
+ import fs25 from "fs";
8393
8538
  import path28 from "path";
8394
- import fs25 from "fs-extra";
8539
+ var NEXT_CONFIG_FILES = ["next.config.ts", "next.config.js", "next.config.mjs"];
8540
+ function detectProjectName(cwd) {
8541
+ const pkgPath = path28.join(cwd, "package.json");
8542
+ if (fs25.existsSync(pkgPath)) {
8543
+ try {
8544
+ const pkg = JSON.parse(fs25.readFileSync(pkgPath, "utf-8"));
8545
+ if (typeof pkg.name === "string" && pkg.name.length > 0) {
8546
+ return formatProjectName(pkg.name);
8547
+ }
8548
+ } catch {
8549
+ }
8550
+ }
8551
+ return formatProjectName(path28.basename(cwd));
8552
+ }
8553
+ function formatProjectName(name) {
8554
+ const base = name.includes("/") ? name.split("/").pop() : name;
8555
+ return base.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
8556
+ }
8557
+ function detectProject(cwd) {
8558
+ const isExisting = NEXT_CONFIG_FILES.some((f) => fs25.existsSync(path28.join(cwd, f)));
8559
+ const hasSrcDir = fs25.existsSync(path28.join(cwd, "src"));
8560
+ const hasTypeScript = fs25.existsSync(path28.join(cwd, "tsconfig.json")) || fs25.existsSync(path28.join(cwd, "tsconfig.app.json"));
8561
+ const hasTailwind = detectTailwind(cwd);
8562
+ const linter = detectLinter(cwd);
8563
+ const conflicts = [];
8564
+ if (isExisting) {
8565
+ if (fs25.existsSync(path28.join(cwd, "cms"))) {
8566
+ conflicts.push("cms/ directory already exists");
8567
+ }
8568
+ if (fs25.existsSync(path28.join(cwd, "cms.config.ts"))) {
8569
+ conflicts.push("cms.config.ts already exists");
8570
+ }
8571
+ const appBase = hasSrcDir ? "src/app" : "app";
8572
+ if (fs25.existsSync(path28.join(cwd, appBase, "(cms)"))) {
8573
+ conflicts.push(`${appBase}/(cms)/ route group already exists`);
8574
+ }
8575
+ if (hasTsconfigCmsAliases(cwd)) {
8576
+ conflicts.push("@cms/* path aliases already exist in tsconfig.json");
8577
+ }
8578
+ if (hasEnvBetterstartVars(cwd)) {
8579
+ conflicts.push("BETTERSTART_* variables already exist in .env.local");
8580
+ }
8581
+ }
8582
+ return { isExisting, hasSrcDir, hasTypeScript, hasTailwind, linter, conflicts };
8583
+ }
8584
+ var BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"];
8585
+ var ESLINT_CONFIG_FILES = [
8586
+ "eslint.config.js",
8587
+ "eslint.config.mjs",
8588
+ "eslint.config.cjs",
8589
+ "eslint.config.ts",
8590
+ ".eslintrc.json",
8591
+ ".eslintrc.js",
8592
+ ".eslintrc.cjs",
8593
+ ".eslintrc.yml",
8594
+ ".eslintrc.yaml",
8595
+ ".eslintrc"
8596
+ ];
8597
+ function detectLinter(cwd) {
8598
+ for (const f of BIOME_CONFIG_FILES) {
8599
+ if (fs25.existsSync(path28.join(cwd, f))) {
8600
+ return { type: "biome", configFile: f };
8601
+ }
8602
+ }
8603
+ for (const f of ESLINT_CONFIG_FILES) {
8604
+ if (fs25.existsSync(path28.join(cwd, f))) {
8605
+ return { type: "eslint", configFile: f };
8606
+ }
8607
+ }
8608
+ const pkgPath = path28.join(cwd, "package.json");
8609
+ if (fs25.existsSync(pkgPath)) {
8610
+ try {
8611
+ const pkg = JSON.parse(fs25.readFileSync(pkgPath, "utf-8"));
8612
+ if (pkg.eslintConfig) {
8613
+ return { type: "eslint", configFile: "package.json (eslintConfig)" };
8614
+ }
8615
+ } catch {
8616
+ }
8617
+ }
8618
+ return { type: "none", configFile: null };
8619
+ }
8620
+ function detectTailwind(cwd) {
8621
+ const cssFiles = ["globals.css", "app.css", "index.css"].flatMap((f) => [
8622
+ path28.join(cwd, "src", "app", f),
8623
+ path28.join(cwd, "app", f),
8624
+ path28.join(cwd, "src", f),
8625
+ path28.join(cwd, f)
8626
+ ]);
8627
+ for (const cssFile of cssFiles) {
8628
+ if (fs25.existsSync(cssFile)) {
8629
+ const content = fs25.readFileSync(cssFile, "utf-8");
8630
+ if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'") || content.includes("@theme")) {
8631
+ return true;
8632
+ }
8633
+ }
8634
+ }
8635
+ const postcssFiles = ["postcss.config.js", "postcss.config.mjs", "postcss.config.cjs"];
8636
+ for (const f of postcssFiles) {
8637
+ if (fs25.existsSync(path28.join(cwd, f))) {
8638
+ const content = fs25.readFileSync(path28.join(cwd, f), "utf-8");
8639
+ if (content.includes("tailwindcss") || content.includes("@tailwindcss")) {
8640
+ return true;
8641
+ }
8642
+ }
8643
+ }
8644
+ return false;
8645
+ }
8646
+ function hasTsconfigCmsAliases(cwd) {
8647
+ const tsconfigPath = path28.join(cwd, "tsconfig.json");
8648
+ if (!fs25.existsSync(tsconfigPath)) return false;
8649
+ try {
8650
+ const content = fs25.readFileSync(tsconfigPath, "utf-8");
8651
+ return content.includes("@cms/");
8652
+ } catch {
8653
+ return false;
8654
+ }
8655
+ }
8656
+ function hasEnvBetterstartVars(cwd) {
8657
+ const envPath = path28.join(cwd, ".env.local");
8658
+ if (!fs25.existsSync(envPath)) return false;
8659
+ try {
8660
+ const content = fs25.readFileSync(envPath, "utf-8");
8661
+ return content.includes("BETTERSTART_");
8662
+ } catch {
8663
+ return false;
8664
+ }
8665
+ }
8395
8666
 
8396
8667
  // src/init/templates/components/cms-globals.ts
8397
8668
  function cmsGlobalsCssTemplate() {
@@ -8507,6 +8778,12 @@ function cmsGlobalsCssTemplate() {
8507
8778
  --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
8508
8779
  }
8509
8780
 
8781
+ .cms-root {
8782
+ --font-sans: var(--font-geist-sans, sans-serif);
8783
+ --font-mono: var(--font-geist-mono, monospace);
8784
+ font-family: var(--font-sans);
8785
+ }
8786
+
8510
8787
  @theme inline {
8511
8788
  --color-background: var(--background);
8512
8789
  --color-foreground: var(--foreground);
@@ -8565,7 +8842,7 @@ function cmsGlobalsCssTemplate() {
8565
8842
  @apply border-border outline-ring/50;
8566
8843
  }
8567
8844
  body {
8568
- @apply bg-background text-foreground;
8845
+ @apply bg-background text-foreground antialiased;
8569
8846
  }
8570
8847
  }
8571
8848
 
@@ -8986,6 +9263,7 @@ export function ReorderControls({
8986
9263
  function cmsHeaderTemplate() {
8987
9264
  return `'use client'
8988
9265
 
9266
+ import { CmsSearch } from '@cms/components/layout/cms-search'
8989
9267
  import { Button } from '@cms/components/ui/button'
8990
9268
  import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
8991
9269
  import { useTheme } from '@cms/hooks/use-cms-theme'
@@ -8998,16 +9276,21 @@ export function CmsHeader() {
8998
9276
  return (
8999
9277
  <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50 bg-sidebar">
9000
9278
  <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
9001
- {state === 'collapsed' && <SidebarTrigger />}
9002
- <Button
9003
- variant="ghost"
9004
- size="icon"
9005
- onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
9006
- >
9007
- <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
9008
- <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
9009
- <span className="sr-only">Toggle theme</span>
9010
- </Button>
9279
+ <div className="flex items-center gap-2 w-full">
9280
+ {state === 'collapsed' && <SidebarTrigger />}
9281
+ <CmsSearch />
9282
+ </div>
9283
+ <div className="flex items-center gap-2 ml-auto">
9284
+ <Button
9285
+ variant="ghost"
9286
+ size="icon"
9287
+ onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
9288
+ >
9289
+ <Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
9290
+ <Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
9291
+ <span className="sr-only">Toggle theme</span>
9292
+ </Button>
9293
+ </div>
9011
9294
  </div>
9012
9295
  </header>
9013
9296
  )
@@ -9053,9 +9336,40 @@ export function CmsProviders({ children }: { children: React.ReactNode }) {
9053
9336
  `;
9054
9337
  }
9055
9338
 
9339
+ // src/init/templates/components/layout/cms-search.ts
9340
+ function cmsSearchTemplate() {
9341
+ return `import { Button } from '@cms/components/ui/button'
9342
+ import { Command, Search } from 'lucide-react'
9343
+
9344
+ export const CmsSearch = () => {
9345
+ return (
9346
+ <div className="flex items-center gap-2 relative w-full max-w-[240px]">
9347
+ <Button
9348
+ variant="outline"
9349
+ className="w-full text-left items-center pr-1! rounded-full"
9350
+ size="sm"
9351
+ >
9352
+ <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground" strokeWidth={2} />
9353
+ <span className="w-full font-medium text-xs pt-px text-muted-foreground leading-0">
9354
+ Find...
9355
+ </span>
9356
+ <div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
9357
+ <Command className="size-3! text-muted-foreground" strokeWidth={1.5} />
9358
+ <span className="font-mono text-xs font-medium">K</span>
9359
+ </div>
9360
+ </Button>
9361
+ </div>
9362
+ )
9363
+ }
9364
+
9365
+ CmsSearch.displayName = 'CmsSearch'
9366
+ `;
9367
+ }
9368
+
9056
9369
  // src/init/templates/components/layout/cms-sidebar.ts
9057
9370
  function cmsSidebarTemplate() {
9058
- return `import { getSession } from '@cms/auth/middleware'
9371
+ return `import { getSetting } from '@/cms/lib/actions/settings'
9372
+ import { getSession } from '@cms/auth/middleware'
9059
9373
  import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
9060
9374
  import {
9061
9375
  Collapsible,
@@ -9076,20 +9390,21 @@ import {
9076
9390
  SidebarRail,
9077
9391
  SidebarTrigger,
9078
9392
  } from '@cms/components/ui/sidebar'
9393
+ import { cms } from '@cms/data/cms'
9079
9394
  import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
9080
- import { ChevronRight } from 'lucide-react'
9395
+ import { ChevronRight, Settings, Users } from 'lucide-react'
9081
9396
  import Link from 'next/link'
9082
9397
 
9083
9398
  function NavItem({ item }: { item: CmsNavigationItem }) {
9084
9399
  if (item.children && item.children.length > 0) {
9085
9400
  return (
9086
- <Collapsible asChild defaultOpen className="group/collapsible">
9401
+ <Collapsible asChild defaultOpen className="group/collapsible border-y border-border py-2 px-2">
9087
9402
  <SidebarMenuItem>
9088
9403
  <CollapsibleTrigger asChild>
9089
9404
  <SidebarMenuButton>
9090
- {item.icon && <item.icon className="h-4 w-4" />}
9405
+ {item.icon && <item.icon className="size-3.5!" />}
9091
9406
  <span>{item.label}</span>
9092
- <ChevronRight className="ml-auto h-4 w-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
9407
+ <ChevronRight className="ml-auto size-3.5! transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
9093
9408
  </SidebarMenuButton>
9094
9409
  </CollapsibleTrigger>
9095
9410
  <CollapsibleContent>
@@ -9098,7 +9413,7 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9098
9413
  <SidebarMenuSubItem key={child.href}>
9099
9414
  <SidebarMenuSubButton asChild>
9100
9415
  <Link href={child.href}>
9101
- {child.icon && <child.icon className="h-4 w-4" />}
9416
+ {child.icon && <child.icon className="size-3.5!" />}
9102
9417
  <span>{child.label}</span>
9103
9418
  </Link>
9104
9419
  </SidebarMenuSubButton>
@@ -9112,10 +9427,10 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9112
9427
  }
9113
9428
 
9114
9429
  return (
9115
- <SidebarMenuItem>
9430
+ <SidebarMenuItem className="px-2">
9116
9431
  <SidebarMenuButton asChild>
9117
9432
  <Link href={item.href}>
9118
- {item.icon && <item.icon className="h-4 w-4" />}
9433
+ {item.icon && <item.icon className="size-3.5!" />}
9119
9434
  <span>{item.label}</span>
9120
9435
  </Link>
9121
9436
  </SidebarMenuButton>
@@ -9125,32 +9440,51 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9125
9440
 
9126
9441
  export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9127
9442
  const session = await getSession()
9443
+ const settings = await getSetting()
9128
9444
  const user = session?.user ?? null
9129
9445
 
9130
9446
  return (
9131
9447
  <Sidebar collapsible="icon" {...props}>
9132
9448
  <SidebarHeader className="border-b border-border h-14 items-center flex w-full">
9133
9449
  <div className="flex items-center gap-2 w-full relative h-full">
9134
- <div className="flex items-center gap-2 w-full">
9135
- <Avatar className="size-8">
9136
- <AvatarImage src={user?.image ?? ''} />
9450
+ <Link href="/cms" className="flex items-center gap-2 w-full">
9451
+ <Avatar className="size-6.5">
9452
+ <AvatarImage src={'/favicon.ico'} />
9137
9453
  <AvatarFallback className="text-sm font-semibold">
9138
- {user?.name?.charAt(0)}
9454
+ {settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
9139
9455
  </AvatarFallback>
9140
9456
  </Avatar>
9141
9457
  <div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9142
- <span className="text-base font-bold">CMS</span>
9458
+ <span className="text-sm font-semibold line-clamp-1">{settings?.siteName ?? cms.name}</span>
9143
9459
  </div>
9144
- </div>
9460
+ </Link>
9145
9461
  <SidebarTrigger className="hidden md:flex" />
9146
9462
  </div>
9147
9463
  </SidebarHeader>
9148
- <SidebarContent>
9149
- <SidebarMenu className="p-2 gap-1">
9464
+ <SidebarContent className="gap-2">
9465
+ <SidebarMenu className="py-2 gap-2">
9150
9466
  {cmsNavigation.map((item) => (
9151
9467
  <NavItem key={item.href + item.label} item={item} />
9152
9468
  ))}
9153
9469
  </SidebarMenu>
9470
+ <SidebarMenu className="py-2 mt-auto border-t border-border px-2">
9471
+ <SidebarMenuItem>
9472
+ <SidebarMenuButton asChild>
9473
+ <Link href="/cms/users">
9474
+ <Users className="size-3.5!" />
9475
+ <span>Users</span>
9476
+ </Link>
9477
+ </SidebarMenuButton>
9478
+ </SidebarMenuItem>
9479
+ <SidebarMenuItem>
9480
+ <SidebarMenuButton asChild>
9481
+ <Link href="/cms/settings">
9482
+ <Settings className="size-3.5!" />
9483
+ <span>Settings</span>
9484
+ </Link>
9485
+ </SidebarMenuButton>
9486
+ </SidebarMenuItem>
9487
+ </SidebarMenu>
9154
9488
  </SidebarContent>
9155
9489
  <SidebarFooter>
9156
9490
  {user && (
@@ -9315,9 +9649,17 @@ export function BooleanBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }: {
9315
9649
  `;
9316
9650
  }
9317
9651
 
9652
+ // src/init/templates/data/cms.ts
9653
+ function cmsDataTemplate(projectName) {
9654
+ return `export const cms = {
9655
+ name: '${projectName}',
9656
+ }
9657
+ `;
9658
+ }
9659
+
9318
9660
  // src/init/templates/data/navigation.ts
9319
9661
  function navigationDataTemplate() {
9320
- return `import { House, Settings, Users } from 'lucide-react'
9662
+ return `import { House } from 'lucide-react'
9321
9663
  import type { LucideIcon } from 'lucide-react'
9322
9664
 
9323
9665
  export interface CmsNavigationItem {
@@ -9332,16 +9674,6 @@ export const cmsNavigation: CmsNavigationItem[] = [
9332
9674
  label: 'Dashboard',
9333
9675
  href: '/cms',
9334
9676
  icon: House
9335
- },
9336
- {
9337
- label: 'Users',
9338
- href: '/cms/users',
9339
- icon: Users
9340
- },
9341
- {
9342
- label: 'Settings',
9343
- href: '/cms/settings',
9344
- icon: Settings
9345
9677
  }
9346
9678
  ]
9347
9679
  `;
@@ -10114,6 +10446,11 @@ export interface UpdateUserRoleResult {
10114
10446
  error?: string
10115
10447
  }
10116
10448
 
10449
+ export interface DeleteUserResult {
10450
+ success: boolean
10451
+ error?: string
10452
+ }
10453
+
10117
10454
  /**
10118
10455
  * Create a new user via Better Auth's built-in API
10119
10456
  */
@@ -10206,6 +10543,21 @@ export async function updateUserRole(
10206
10543
  }
10207
10544
  }
10208
10545
  }
10546
+
10547
+ /**
10548
+ * Delete a user
10549
+ */
10550
+ export async function deleteUser(userId: string): Promise<DeleteUserResult> {
10551
+ try {
10552
+ await db.delete(user).where(eq(user.id, userId))
10553
+ return { success: true }
10554
+ } catch (error) {
10555
+ return {
10556
+ success: false,
10557
+ error: error instanceof Error ? error.message : 'Failed to delete user',
10558
+ }
10559
+ }
10560
+ }
10209
10561
  `;
10210
10562
  }
10211
10563
 
@@ -10926,18 +11278,19 @@ export function sendWebhook(
10926
11278
 
10927
11279
  // src/init/scaffolders/components.ts
10928
11280
  function scaffoldComponents({ cwd, config }) {
10929
- const cms = path28.resolve(cwd, config.paths.cms);
11281
+ const cms = path29.resolve(cwd, config.paths.cms);
10930
11282
  const created = [];
10931
11283
  function write(relPath, content) {
10932
- const fullPath = path28.join(cms, relPath);
11284
+ const fullPath = path29.join(cms, relPath);
10933
11285
  if (safeWriteFile(fullPath, content)) {
10934
- created.push(path28.join(config.paths.cms, relPath));
11286
+ created.push(path29.join(config.paths.cms, relPath));
10935
11287
  }
10936
11288
  }
10937
11289
  write("cms-globals.css", cmsGlobalsCssTemplate());
10938
11290
  write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
10939
11291
  write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
10940
11292
  write("components/layout/cms-header.tsx", cmsHeaderTemplate());
11293
+ write("components/layout/cms-search.tsx", cmsSearchTemplate());
10941
11294
  write("components/shared/page-header.tsx", pageHeaderTemplate());
10942
11295
  write("components/shared/delete-dialog.tsx", deleteDialogTemplate());
10943
11296
  write("components/shared/status-badge.tsx", statusBadgeTemplate());
@@ -10960,6 +11313,8 @@ function scaffoldComponents({ cwd, config }) {
10960
11313
  write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
10961
11314
  write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
10962
11315
  write("hooks/use-users.ts", useUsersHookTemplate());
11316
+ const projectName = detectProjectName(cwd);
11317
+ write("data/cms.ts", cmsDataTemplate(projectName));
10963
11318
  write("data/navigation.ts", navigationDataTemplate());
10964
11319
  write("lib/r2.ts", r2ClientTemplate());
10965
11320
  write("lib/actions/form-settings.ts", formSettingsActionTemplate());
@@ -10974,18 +11329,18 @@ function scaffoldComponents({ cwd, config }) {
10974
11329
  }
10975
11330
  function copyUiTemplates(cwd, config) {
10976
11331
  const created = [];
10977
- const destDir = path28.resolve(cwd, config.paths.cms, "components", "ui");
11332
+ const destDir = path29.resolve(cwd, config.paths.cms, "components", "ui");
10978
11333
  const cliRoot = findCliRoot();
10979
- const srcDir = path28.join(cliRoot, "templates", "ui");
10980
- if (!fs25.existsSync(srcDir)) {
11334
+ const srcDir = path29.join(cliRoot, "templates", "ui");
11335
+ if (!fs26.existsSync(srcDir)) {
10981
11336
  return created;
10982
11337
  }
10983
- const files = fs25.readdirSync(srcDir).filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"));
11338
+ const files = fs26.readdirSync(srcDir).filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"));
10984
11339
  for (const file of files) {
10985
- const destPath = path28.join(destDir, file);
10986
- if (!fs25.existsSync(destPath)) {
10987
- fs25.copyFileSync(path28.join(srcDir, file), destPath);
10988
- created.push(path28.join(config.paths.cms, "components", "ui", file));
11340
+ const destPath = path29.join(destDir, file);
11341
+ if (!fs26.existsSync(destPath)) {
11342
+ fs26.copyFileSync(path29.join(srcDir, file), destPath);
11343
+ created.push(path29.join(config.paths.cms, "components", "ui", file));
10989
11344
  }
10990
11345
  }
10991
11346
  return created;
@@ -10993,25 +11348,25 @@ function copyUiTemplates(cwd, config) {
10993
11348
  function copyTiptapTemplates(cwd, config) {
10994
11349
  const created = [];
10995
11350
  const cliRoot = findCliRoot();
10996
- const srcDir = path28.join(cliRoot, "templates", "tiptap");
10997
- const destDir = path28.resolve(cwd, config.paths.cms, "components", "ui", "tiptap");
10998
- if (!fs25.existsSync(srcDir)) {
11351
+ const srcDir = path29.join(cliRoot, "templates", "tiptap");
11352
+ const destDir = path29.resolve(cwd, config.paths.cms, "components", "ui", "tiptap");
11353
+ if (!fs26.existsSync(srcDir)) {
10999
11354
  return created;
11000
11355
  }
11001
11356
  copyDirRecursive(srcDir, destDir, config.paths.cms, created);
11002
11357
  return created;
11003
11358
  }
11004
11359
  function copyDirRecursive(src, dest, cmsPrefix, created) {
11005
- fs25.ensureDirSync(dest);
11006
- const entries = fs25.readdirSync(src, { withFileTypes: true });
11360
+ fs26.ensureDirSync(dest);
11361
+ const entries = fs26.readdirSync(src, { withFileTypes: true });
11007
11362
  for (const entry of entries) {
11008
- const srcPath = path28.join(src, entry.name);
11009
- const destPath = path28.join(dest, entry.name);
11363
+ const srcPath = path29.join(src, entry.name);
11364
+ const destPath = path29.join(dest, entry.name);
11010
11365
  if (entry.isDirectory()) {
11011
11366
  copyDirRecursive(srcPath, destPath, cmsPrefix, created);
11012
- } else if (!fs25.existsSync(destPath)) {
11013
- fs25.copyFileSync(srcPath, destPath);
11014
- const relFromCms = path28.relative(path28.resolve(dest, "..", "..", "..", ".."), destPath);
11367
+ } else if (!fs26.existsSync(destPath)) {
11368
+ fs26.copyFileSync(srcPath, destPath);
11369
+ const relFromCms = path29.relative(path29.resolve(dest, "..", "..", "..", ".."), destPath);
11015
11370
  created.push(relFromCms);
11016
11371
  }
11017
11372
  }
@@ -11019,35 +11374,35 @@ function copyDirRecursive(src, dest, cmsPrefix, created) {
11019
11374
  function copySchemaMetaschema(cwd, config) {
11020
11375
  const created = [];
11021
11376
  const cliRoot = findCliRoot();
11022
- const srcPath = path28.join(cliRoot, "templates", "schema.json");
11023
- const destPath = path28.resolve(cwd, config.paths.schemas, "schema.json");
11024
- if (fs25.existsSync(srcPath) && !fs25.existsSync(destPath)) {
11025
- fs25.ensureDirSync(path28.dirname(destPath));
11026
- fs25.copyFileSync(srcPath, destPath);
11027
- created.push(path28.join(config.paths.schemas, "schema.json"));
11377
+ const srcPath = path29.join(cliRoot, "templates", "schema.json");
11378
+ const destPath = path29.resolve(cwd, config.paths.schemas, "schema.json");
11379
+ if (fs26.existsSync(srcPath) && !fs26.existsSync(destPath)) {
11380
+ fs26.ensureDirSync(path29.dirname(destPath));
11381
+ fs26.copyFileSync(srcPath, destPath);
11382
+ created.push(path29.join(config.paths.schemas, "schema.json"));
11028
11383
  }
11029
11384
  return created;
11030
11385
  }
11031
11386
  function findCliRoot() {
11032
11387
  let dir = new URL(".", import.meta.url).pathname;
11033
11388
  for (let i = 0; i < 5; i++) {
11034
- const pkgPath = path28.join(dir, "package.json");
11035
- if (fs25.existsSync(pkgPath)) {
11389
+ const pkgPath = path29.join(dir, "package.json");
11390
+ if (fs26.existsSync(pkgPath)) {
11036
11391
  try {
11037
- const pkg = JSON.parse(fs25.readFileSync(pkgPath, "utf-8"));
11392
+ const pkg = JSON.parse(fs26.readFileSync(pkgPath, "utf-8"));
11038
11393
  if (pkg.name === "@betterstart/cli") {
11039
11394
  return dir;
11040
11395
  }
11041
11396
  } catch {
11042
11397
  }
11043
11398
  }
11044
- dir = path28.dirname(dir);
11399
+ dir = path29.dirname(dir);
11045
11400
  }
11046
- return path28.resolve(new URL(".", import.meta.url).pathname, "..", "..");
11401
+ return path29.resolve(new URL(".", import.meta.url).pathname, "..", "..");
11047
11402
  }
11048
11403
 
11049
11404
  // src/init/scaffolders/database.ts
11050
- import path29 from "path";
11405
+ import path30 from "path";
11051
11406
 
11052
11407
  // src/init/templates/db/client.ts
11053
11408
  function dbClientTemplate() {
@@ -11176,16 +11531,16 @@ export const formSettings = pgTable(
11176
11531
  // src/init/scaffolders/database.ts
11177
11532
  function scaffoldDatabase({ cwd, config }) {
11178
11533
  const created = [];
11179
- const dbDir = path29.resolve(cwd, config.paths.cms, "db");
11534
+ const dbDir = path30.resolve(cwd, config.paths.cms, "db");
11180
11535
  function write(filename, content) {
11181
- const fullPath = path29.join(dbDir, filename);
11536
+ const fullPath = path30.join(dbDir, filename);
11182
11537
  if (safeWriteFile(fullPath, content)) {
11183
- created.push(path29.join(config.paths.cms, "db", filename));
11538
+ created.push(path30.join(config.paths.cms, "db", filename));
11184
11539
  }
11185
11540
  }
11186
11541
  write("client.ts", dbClientTemplate());
11187
11542
  write("schema.ts", dbSchemaTemplate());
11188
- const drizzleConfigPath = path29.resolve(cwd, "drizzle.config.ts");
11543
+ const drizzleConfigPath = path30.resolve(cwd, "drizzle.config.ts");
11189
11544
  if (safeWriteFile(drizzleConfigPath, drizzleConfigTemplate())) {
11190
11545
  created.push("drizzle.config.ts");
11191
11546
  }
@@ -11348,25 +11703,41 @@ async function installDependenciesAsync({
11348
11703
  }
11349
11704
  }
11350
11705
 
11706
+ // src/init/scaffolders/env.ts
11707
+ import crypto from "crypto";
11708
+
11351
11709
  // src/utils/env.ts
11352
- import fs26 from "fs";
11353
- import path30 from "path";
11354
- function appendEnvVars(cwd, sections) {
11355
- const envPath = path30.join(cwd, ".env.local");
11356
- const existing = fs26.existsSync(envPath) ? fs26.readFileSync(envPath, "utf-8") : "";
11710
+ import fs27 from "fs";
11711
+ import path31 from "path";
11712
+ function appendEnvVars(cwd, sections, overwrite) {
11713
+ const envPath = path31.join(cwd, ".env.local");
11714
+ let existing = fs27.existsSync(envPath) ? fs27.readFileSync(envPath, "utf-8") : "";
11357
11715
  const existingKeys = new Set(
11358
11716
  existing.split("\n").filter((line) => line.trim() && !line.trim().startsWith("#")).map((line) => line.split("=")[0]?.trim()).filter(Boolean)
11359
11717
  );
11360
11718
  const added = [];
11361
11719
  const skipped = [];
11720
+ const updated = [];
11362
11721
  const lines = [];
11722
+ if (overwrite) {
11723
+ for (const section of sections) {
11724
+ for (const v of section.vars) {
11725
+ if (overwrite.has(v.key) && existingKeys.has(v.key)) {
11726
+ const pattern = new RegExp(`^${v.key}=.*$`, "m");
11727
+ const replacement = v.comment ? `${v.key}="${v.value}" # ${v.comment}` : `${v.key}="${v.value}"`;
11728
+ existing = existing.replace(pattern, replacement);
11729
+ updated.push(v.key);
11730
+ }
11731
+ }
11732
+ }
11733
+ }
11363
11734
  if (existing.trim()) {
11364
11735
  lines.push("");
11365
11736
  }
11366
11737
  for (const section of sections) {
11367
11738
  const sectionVars = section.vars.filter((v) => {
11368
11739
  if (existingKeys.has(v.key)) {
11369
- skipped.push(v.key);
11740
+ if (!updated.includes(v.key)) skipped.push(v.key);
11370
11741
  return false;
11371
11742
  }
11372
11743
  added.push(v.key);
@@ -11380,26 +11751,27 @@ function appendEnvVars(cwd, sections) {
11380
11751
  }
11381
11752
  lines.push("");
11382
11753
  }
11383
- if (added.length > 0) {
11754
+ if (added.length > 0 || updated.length > 0) {
11384
11755
  const header = existing.trim() ? "" : "# ============================================\n# BetterStart CMS\n# ============================================\n";
11385
11756
  const content = existing.trim() ? `${existing.trimEnd()}
11386
11757
  ${lines.join("\n")}` : header + lines.join("\n");
11387
- fs26.writeFileSync(envPath, content);
11758
+ fs27.writeFileSync(envPath, content);
11388
11759
  }
11389
- return { added, skipped };
11760
+ return { added, skipped, updated };
11390
11761
  }
11391
11762
 
11392
11763
  // src/init/scaffolders/env.ts
11393
- function getCoreEnvSections() {
11764
+ function getCoreEnvSections(databaseUrl) {
11765
+ const authSecret = crypto.randomBytes(32).toString("base64");
11394
11766
  return [
11395
11767
  {
11396
11768
  header: "Database (Neon)",
11397
- vars: [{ key: "BETTERSTART_DATABASE_URL", value: "postgresql://..." }]
11769
+ vars: [{ key: "BETTERSTART_DATABASE_URL", value: databaseUrl ?? "postgresql://..." }]
11398
11770
  },
11399
11771
  {
11400
11772
  header: "Authentication",
11401
11773
  vars: [
11402
- { key: "BETTERSTART_AUTH_SECRET", value: "", comment: "openssl rand -base64 32" },
11774
+ { key: "BETTERSTART_AUTH_SECRET", value: authSecret },
11403
11775
  { key: "BETTERSTART_AUTH_URL", value: "http://localhost:3000" },
11404
11776
  { key: "BETTERSTART_AUTH_BASE_PATH", value: "/api/cms/auth" }
11405
11777
  ]
@@ -11426,15 +11798,16 @@ function getEmailEnvSection() {
11426
11798
  };
11427
11799
  }
11428
11800
  function scaffoldEnv(cwd, options) {
11429
- const sections = getCoreEnvSections();
11801
+ const sections = getCoreEnvSections(options.databaseUrl);
11430
11802
  if (options.includeEmail) {
11431
11803
  sections.push(getEmailEnvSection());
11432
11804
  }
11433
- return appendEnvVars(cwd, sections);
11805
+ const overwrite = options.databaseUrl ? /* @__PURE__ */ new Set(["BETTERSTART_DATABASE_URL"]) : void 0;
11806
+ return appendEnvVars(cwd, sections, overwrite);
11434
11807
  }
11435
11808
 
11436
11809
  // src/init/scaffolders/layout.ts
11437
- import path31 from "path";
11810
+ import path32 from "path";
11438
11811
 
11439
11812
  // src/init/templates/pages/authenticated-layout.ts
11440
11813
  function authenticatedLayoutTemplate() {
@@ -11930,23 +12303,38 @@ export function EditRoleDialog({
11930
12303
  function usersColumnsTemplate() {
11931
12304
  return `'use client'
11932
12305
 
12306
+ import React from 'react'
11933
12307
  import {
11934
12308
  Avatar,
11935
12309
  AvatarFallback
11936
12310
  } from '@cms/components/ui/avatar'
11937
- import { Badge } from '@cms/components/ui/badge'
11938
- import { Button } from '@cms/components/ui/button'
11939
12311
  import {
11940
- DropdownMenu,
11941
- DropdownMenuContent,
11942
- DropdownMenuItem,
12312
+ AlertDialog,
12313
+ AlertDialogAction,
12314
+ AlertDialogCancel,
12315
+ AlertDialogContent,
12316
+ AlertDialogDescription,
12317
+ AlertDialogFooter,
12318
+ AlertDialogHeader,
12319
+ AlertDialogTitle,
12320
+ AlertDialogTrigger,
12321
+ } from '@cms/components/ui/alert-dialog'
12322
+ import { Badge } from '@cms/components/ui/badge'
12323
+ import { Button } from '@cms/components/ui/button'
12324
+ import {
12325
+ DropdownMenu,
12326
+ DropdownMenuContent,
12327
+ DropdownMenuItem,
11943
12328
  DropdownMenuLabel,
11944
12329
  DropdownMenuSeparator,
11945
12330
  DropdownMenuTrigger
11946
12331
  } from '@cms/components/ui/dropdown-menu'
11947
12332
  import type { UserData } from '@cms/types/auth'
11948
12333
  import type { ColumnDef } from '@tanstack/react-table'
11949
- import { ArrowUpDown, Edit, MoreHorizontal } from 'lucide-react'
12334
+ import { useQueryClient } from '@tanstack/react-query'
12335
+ import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
12336
+ import { toast } from 'sonner'
12337
+ import { deleteUser } from '@cms/actions/users'
11950
12338
  import { EditRoleDialog } from './edit-role-dialog'
11951
12339
 
11952
12340
  function getInitials(nameOrEmail: string): string {
@@ -11960,6 +12348,77 @@ function getInitials(nameOrEmail: string): string {
11960
12348
  .join('')
11961
12349
  }
11962
12350
 
12351
+ function DeleteUserAction({
12352
+ userId,
12353
+ userName,
12354
+ isCurrentUser,
12355
+ }: {
12356
+ userId: string
12357
+ userName: string
12358
+ isCurrentUser: boolean
12359
+ }) {
12360
+ const [open, setOpen] = React.useState(false)
12361
+ const [isPending, startTransition] = React.useTransition()
12362
+ const queryClient = useQueryClient()
12363
+
12364
+ if (isCurrentUser) return null
12365
+
12366
+ const handleDelete = () => {
12367
+ startTransition(async () => {
12368
+ try {
12369
+ const result = await deleteUser(userId)
12370
+
12371
+ if (result.success) {
12372
+ toast.success('User deleted successfully')
12373
+ queryClient.refetchQueries({ queryKey: ['users'] })
12374
+ setOpen(false)
12375
+ } else {
12376
+ toast.error(result.error || 'Failed to delete user')
12377
+ }
12378
+ } catch (error) {
12379
+ toast.error('An error occurred')
12380
+ console.error(error)
12381
+ }
12382
+ })
12383
+ }
12384
+
12385
+ return (
12386
+ <AlertDialog open={open} onOpenChange={setOpen}>
12387
+ <AlertDialogTrigger asChild>
12388
+ <DropdownMenuItem
12389
+ className="text-destructive"
12390
+ onSelect={(e) => e.preventDefault()}
12391
+ >
12392
+ <Trash className="size-4 mr-2" />
12393
+ Delete user
12394
+ </DropdownMenuItem>
12395
+ </AlertDialogTrigger>
12396
+ <AlertDialogContent>
12397
+ <AlertDialogHeader>
12398
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
12399
+ <AlertDialogDescription>
12400
+ This action cannot be undone. This will permanently delete{' '}
12401
+ <strong>{userName}</strong> and all of their data.
12402
+ </AlertDialogDescription>
12403
+ </AlertDialogHeader>
12404
+ <AlertDialogFooter>
12405
+ <AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
12406
+ <AlertDialogAction
12407
+ onClick={(e) => {
12408
+ e.preventDefault()
12409
+ handleDelete()
12410
+ }}
12411
+ disabled={isPending}
12412
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
12413
+ >
12414
+ {isPending ? 'Deleting...' : 'Delete'}
12415
+ </AlertDialogAction>
12416
+ </AlertDialogFooter>
12417
+ </AlertDialogContent>
12418
+ </AlertDialog>
12419
+ )
12420
+ }
12421
+
11963
12422
  export const columns: ColumnDef<UserData>[] = [
11964
12423
  {
11965
12424
  accessorKey: 'email',
@@ -12058,30 +12517,37 @@ export const columns: ColumnDef<UserData>[] = [
12058
12517
  },
12059
12518
  {
12060
12519
  id: 'actions',
12061
- cell: ({ row }) => (
12062
- <div className="flex justify-end">
12063
- <DropdownMenu>
12064
- <DropdownMenuTrigger asChild>
12065
- <Button variant="ghost" className="size-8 p-0">
12066
- <span className="sr-only">Open menu</span>
12067
- <MoreHorizontal className="size-4" />
12068
- </Button>
12069
- </DropdownMenuTrigger>
12070
- <DropdownMenuContent align="end">
12071
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
12072
- <DropdownMenuItem
12073
- onClick={() => navigator.clipboard.writeText(row.original.id)}
12074
- >
12075
- Copy user ID
12076
- </DropdownMenuItem>
12077
- <DropdownMenuSeparator />
12078
- <DropdownMenuItem className="text-destructive">
12079
- Delete user
12080
- </DropdownMenuItem>
12081
- </DropdownMenuContent>
12082
- </DropdownMenu>
12083
- </div>
12084
- )
12520
+ cell: ({ row, table }) => {
12521
+ const currentUser = table.options.meta?.currentUser
12522
+ const isCurrentUser = currentUser?.email === row.original.email
12523
+
12524
+ return (
12525
+ <div className="flex justify-end">
12526
+ <DropdownMenu>
12527
+ <DropdownMenuTrigger asChild>
12528
+ <Button variant="ghost" className="size-8 p-0">
12529
+ <span className="sr-only">Open menu</span>
12530
+ <MoreHorizontal className="size-4" />
12531
+ </Button>
12532
+ </DropdownMenuTrigger>
12533
+ <DropdownMenuContent align="end">
12534
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
12535
+ <DropdownMenuItem
12536
+ onClick={() => navigator.clipboard.writeText(row.original.id)}
12537
+ >
12538
+ Copy user ID
12539
+ </DropdownMenuItem>
12540
+ <DropdownMenuSeparator />
12541
+ <DeleteUserAction
12542
+ userId={row.original.id}
12543
+ userName={row.original.name}
12544
+ isCurrentUser={isCurrentUser}
12545
+ />
12546
+ </DropdownMenuContent>
12547
+ </DropdownMenu>
12548
+ </div>
12549
+ )
12550
+ }
12085
12551
  }
12086
12552
  ]
12087
12553
  `;
@@ -12100,7 +12566,7 @@ export default function UsersPage() {
12100
12566
  <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
12101
12567
  <PageHeader
12102
12568
  title="Users"
12103
- description="Manage and view all registered users"
12569
+ description="Manage all CMS users"
12104
12570
  >
12105
12571
  <CreateUserDialog />
12106
12572
  </PageHeader>
@@ -12260,30 +12726,30 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
12260
12726
  function scaffoldLayout({ cwd, config }) {
12261
12727
  const created = [];
12262
12728
  function write(relPath, content) {
12263
- const fullPath = path31.resolve(cwd, relPath);
12264
- ensureDir(path31.dirname(fullPath));
12729
+ const fullPath = path32.resolve(cwd, relPath);
12730
+ ensureDir(path32.dirname(fullPath));
12265
12731
  if (safeWriteFile(fullPath, content)) {
12266
12732
  created.push(relPath);
12267
12733
  }
12268
12734
  }
12269
- const cmsDir = path31.dirname(config.paths.pages);
12270
- write(path31.join(cmsDir, "layout.tsx"), cmsLayoutTemplate());
12271
- write(path31.join(config.paths.pages, "layout.tsx"), authenticatedLayoutTemplate());
12272
- write(path31.join(config.paths.login, "page.tsx"), loginPageTemplate());
12273
- write(path31.join(config.paths.login, "login-form.tsx"), loginFormTemplate());
12274
- write(path31.join(config.paths.pages, "page.tsx"), dashboardPageTemplate());
12275
- const usersDir = path31.join(config.paths.pages, "users");
12276
- write(path31.join(usersDir, "page.tsx"), usersPageTemplate());
12277
- write(path31.join(usersDir, "users-table.tsx"), usersTableTemplate());
12278
- write(path31.join(usersDir, "columns.tsx"), usersColumnsTemplate());
12279
- write(path31.join(usersDir, "create-user-dialog.tsx"), createUserDialogTemplate());
12280
- write(path31.join(usersDir, "edit-role-dialog.tsx"), editRoleDialogTemplate());
12735
+ const cmsDir = path32.dirname(config.paths.pages);
12736
+ write(path32.join(cmsDir, "layout.tsx"), cmsLayoutTemplate());
12737
+ write(path32.join(config.paths.pages, "layout.tsx"), authenticatedLayoutTemplate());
12738
+ write(path32.join(config.paths.login, "page.tsx"), loginPageTemplate());
12739
+ write(path32.join(config.paths.login, "login-form.tsx"), loginFormTemplate());
12740
+ write(path32.join(config.paths.pages, "page.tsx"), dashboardPageTemplate());
12741
+ const usersDir = path32.join(config.paths.pages, "users");
12742
+ write(path32.join(usersDir, "page.tsx"), usersPageTemplate());
12743
+ write(path32.join(usersDir, "users-table.tsx"), usersTableTemplate());
12744
+ write(path32.join(usersDir, "columns.tsx"), usersColumnsTemplate());
12745
+ write(path32.join(usersDir, "create-user-dialog.tsx"), createUserDialogTemplate());
12746
+ write(path32.join(usersDir, "edit-role-dialog.tsx"), editRoleDialogTemplate());
12281
12747
  return created;
12282
12748
  }
12283
12749
 
12284
12750
  // src/init/scaffolders/preset.ts
12285
- import fs27 from "fs";
12286
- import path32 from "path";
12751
+ import fs28 from "fs";
12752
+ import path33 from "path";
12287
12753
 
12288
12754
  // src/init/templates/presets/blog-categories.ts
12289
12755
  function blogCategoriesSchema() {
@@ -12293,6 +12759,7 @@ function blogCategoriesSchema() {
12293
12759
  label: "Categories",
12294
12760
  description: "Organize posts with categories",
12295
12761
  icon: "Tag",
12762
+ navGroup: { label: "Blog", icon: "BookOpen" },
12296
12763
  fields: [
12297
12764
  {
12298
12765
  name: "name",
@@ -12374,6 +12841,7 @@ function blogPostsSchema() {
12374
12841
  label: "Posts",
12375
12842
  description: "Manage blog posts and articles",
12376
12843
  icon: "FileText",
12844
+ navGroup: { label: "Blog", icon: "BookOpen" },
12377
12845
  fields: [
12378
12846
  {
12379
12847
  name: "title",
@@ -12469,17 +12937,11 @@ function defaultSettingsSchema() {
12469
12937
  description: "General site settings",
12470
12938
  icon: "Settings",
12471
12939
  fields: [
12472
- { name: "siteName", type: "string", label: "Site Name", required: true },
12940
+ { name: "siteName", type: "string", label: "Site Name", default: "BetterStart" },
12473
12941
  { name: "tagline", type: "string", label: "Tagline" },
12474
12942
  { name: "separator1", type: "separator" },
12475
12943
  { name: "logo", type: "image", label: "Logo" },
12476
- { name: "favicon", type: "image", label: "Favicon" },
12477
- { name: "separator2", type: "separator" },
12478
- { name: "contactEmail", type: "string", label: "Contact Email" },
12479
- { name: "socialTwitter", type: "string", label: "Twitter / X URL" },
12480
- { name: "socialInstagram", type: "string", label: "Instagram URL" },
12481
- { name: "socialLinkedin", type: "string", label: "LinkedIn URL" },
12482
- { name: "socialGithub", type: "string", label: "GitHub URL" }
12944
+ { name: "favicon", type: "image", label: "Favicon" }
12483
12945
  ]
12484
12946
  },
12485
12947
  null,
@@ -12691,15 +13153,15 @@ function scaffoldPreset({
12691
13153
  generatedFiles: [],
12692
13154
  errors: []
12693
13155
  };
12694
- const schemasDir = path32.join(cwd, config.paths?.schemas ?? "./cms/schemas");
13156
+ const schemasDir = path33.join(cwd, config.paths?.schemas ?? "./cms/schemas");
12695
13157
  const presetSchemas = getPresetSchemas(preset);
12696
13158
  for (const ps of presetSchemas) {
12697
- const filePath = path32.join(schemasDir, ps.filename);
12698
- const dir = path32.dirname(filePath);
12699
- if (!fs27.existsSync(dir)) {
12700
- fs27.mkdirSync(dir, { recursive: true });
13159
+ const filePath = path33.join(schemasDir, ps.filename);
13160
+ const dir = path33.dirname(filePath);
13161
+ if (!fs28.existsSync(dir)) {
13162
+ fs28.mkdirSync(dir, { recursive: true });
12701
13163
  }
12702
- fs27.writeFileSync(filePath, ps.content, "utf-8");
13164
+ fs28.writeFileSync(filePath, ps.content, "utf-8");
12703
13165
  result.schemas.push(ps.filename);
12704
13166
  }
12705
13167
  for (const ps of presetSchemas) {
@@ -12734,8 +13196,8 @@ function scaffoldPreset({
12734
13196
  }
12735
13197
 
12736
13198
  // src/init/scaffolders/tailwind.ts
12737
- import fs28 from "fs";
12738
- import path33 from "path";
13199
+ import fs29 from "fs";
13200
+ import path34 from "path";
12739
13201
  var SOURCE_LINES = ['@source "../cms/**/*.{ts,tsx}";', '@source "./(cms)/**/*.{ts,tsx}";'];
12740
13202
  var SOURCE_LINES_SRC = ['@source "../../cms/**/*.{ts,tsx}";', '@source "./(cms)/**/*.{ts,tsx}";'];
12741
13203
  var CMS_THEME_BLOCK = `
@@ -12790,8 +13252,8 @@ function findMainCss(cwd) {
12790
13252
  "globals.css"
12791
13253
  ];
12792
13254
  for (const candidate of candidates) {
12793
- const filePath = path33.join(cwd, candidate);
12794
- if (fs28.existsSync(filePath)) {
13255
+ const filePath = path34.join(cwd, candidate);
13256
+ if (fs29.existsSync(filePath)) {
12795
13257
  return filePath;
12796
13258
  }
12797
13259
  }
@@ -12802,7 +13264,7 @@ function scaffoldTailwind(cwd, hasSrcDir) {
12802
13264
  if (!cssFile) {
12803
13265
  return { file: null, appended: false };
12804
13266
  }
12805
- let content = fs28.readFileSync(cssFile, "utf-8");
13267
+ let content = fs29.readFileSync(cssFile, "utf-8");
12806
13268
  let changed = false;
12807
13269
  const sourceLines = hasSrcDir ? SOURCE_LINES_SRC : SOURCE_LINES;
12808
13270
  const missingLines = sourceLines.filter((sl) => !content.includes(sl));
@@ -12854,14 +13316,14 @@ ${CMS_THEME_BLOCK}
12854
13316
  }
12855
13317
  }
12856
13318
  if (changed) {
12857
- fs28.writeFileSync(cssFile, content, "utf-8");
13319
+ fs29.writeFileSync(cssFile, content, "utf-8");
12858
13320
  }
12859
13321
  return { file: cssFile, appended: changed };
12860
13322
  }
12861
13323
 
12862
13324
  // src/init/scaffolders/tsconfig.ts
12863
- import fs29 from "fs";
12864
- import path34 from "path";
13325
+ import fs30 from "fs";
13326
+ import path35 from "path";
12865
13327
  function stripJsonComments(input) {
12866
13328
  let result = "";
12867
13329
  let i = 0;
@@ -12911,14 +13373,14 @@ var CMS_PATH_ALIASES = {
12911
13373
  "@cms/cache/*": ["./cms/lib/cache/*"]
12912
13374
  };
12913
13375
  function scaffoldTsconfig(cwd) {
12914
- const tsconfigPath = path34.join(cwd, "tsconfig.json");
13376
+ const tsconfigPath = path35.join(cwd, "tsconfig.json");
12915
13377
  const added = [];
12916
13378
  const skipped = [];
12917
- if (!fs29.existsSync(tsconfigPath)) {
13379
+ if (!fs30.existsSync(tsconfigPath)) {
12918
13380
  skipped.push("tsconfig.json not found");
12919
13381
  return { added, skipped };
12920
13382
  }
12921
- const raw = fs29.readFileSync(tsconfigPath, "utf-8");
13383
+ const raw = fs30.readFileSync(tsconfigPath, "utf-8");
12922
13384
  const stripped = stripJsonComments(raw).replace(/,\s*([\]}])/g, "$1");
12923
13385
  let tsconfig;
12924
13386
  try {
@@ -12939,350 +13401,567 @@ function scaffoldTsconfig(cwd) {
12939
13401
  }
12940
13402
  compilerOptions.paths = paths;
12941
13403
  tsconfig.compilerOptions = compilerOptions;
12942
- fs29.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
13404
+ fs30.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
12943
13405
  `, "utf-8");
12944
13406
  return { added, skipped };
12945
13407
  }
12946
13408
 
12947
- // src/utils/detect.ts
12948
- import fs30 from "fs";
12949
- import path35 from "path";
12950
- var NEXT_CONFIG_FILES = ["next.config.ts", "next.config.js", "next.config.mjs"];
12951
- function detectProject(cwd) {
12952
- const isExisting = NEXT_CONFIG_FILES.some((f) => fs30.existsSync(path35.join(cwd, f)));
12953
- const hasSrcDir = fs30.existsSync(path35.join(cwd, "src"));
12954
- const hasTypeScript = fs30.existsSync(path35.join(cwd, "tsconfig.json")) || fs30.existsSync(path35.join(cwd, "tsconfig.app.json"));
12955
- const hasTailwind = detectTailwind(cwd);
12956
- const linter = detectLinter(cwd);
12957
- const conflicts = [];
12958
- if (isExisting) {
12959
- if (fs30.existsSync(path35.join(cwd, "cms"))) {
12960
- conflicts.push("cms/ directory already exists");
12961
- }
12962
- if (fs30.existsSync(path35.join(cwd, "cms.config.ts"))) {
12963
- conflicts.push("cms.config.ts already exists");
12964
- }
12965
- const appBase = hasSrcDir ? "src/app" : "app";
12966
- if (fs30.existsSync(path35.join(cwd, appBase, "(cms)"))) {
12967
- conflicts.push(`${appBase}/(cms)/ route group already exists`);
12968
- }
12969
- if (hasTsconfigCmsAliases(cwd)) {
12970
- conflicts.push("@cms/* path aliases already exist in tsconfig.json");
12971
- }
12972
- if (hasEnvBetterstartVars(cwd)) {
12973
- conflicts.push("BETTERSTART_* variables already exist in .env.local");
12974
- }
13409
+ // src/commands/seed.ts
13410
+ import fs31 from "fs";
13411
+ import path36 from "path";
13412
+ import * as clack from "@clack/prompts";
13413
+ import { Command as Command2 } from "commander";
13414
+ function buildSeedScript() {
13415
+ return `/**
13416
+ * BetterStart CMS \u2014 Seed Script
13417
+ * Creates the initial admin user
13418
+ * AUTO-GENERATED \u2014 safe to delete after running
13419
+ */
13420
+
13421
+ import { loadEnvConfig } from '@next/env'
13422
+ loadEnvConfig(process.cwd())
13423
+
13424
+ import { neon } from '@neondatabase/serverless'
13425
+ import { drizzle } from 'drizzle-orm/neon-http'
13426
+ import { eq } from 'drizzle-orm'
13427
+ import * as schema from '../db/schema'
13428
+ import { betterAuth } from 'better-auth'
13429
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
13430
+
13431
+ // Inline DB connection (mirrors cms/db/client.ts)
13432
+ const sql = neon(process.env.BETTERSTART_DATABASE_URL!)
13433
+ const db = drizzle({ client: sql, schema })
13434
+
13435
+ // Inline auth setup (mirrors cms/lib/auth/auth.ts)
13436
+ const auth = betterAuth({
13437
+ secret: process.env.BETTERSTART_AUTH_SECRET,
13438
+ baseURL: process.env.BETTERSTART_AUTH_URL,
13439
+ basePath: process.env.BETTERSTART_AUTH_BASE_PATH || '/api/cms/auth',
13440
+ database: drizzleAdapter(db, {
13441
+ provider: 'pg',
13442
+ schema: {
13443
+ user: schema.user,
13444
+ session: schema.session,
13445
+ account: schema.account,
13446
+ verification: schema.verification,
13447
+ },
13448
+ }),
13449
+ emailAndPassword: { enabled: true, minPasswordLength: 8 },
13450
+ user: {
13451
+ additionalFields: {
13452
+ role: { type: 'string', required: false, defaultValue: 'member', input: false },
13453
+ },
13454
+ },
13455
+ })
13456
+
13457
+ const EMAIL = process.env.SEED_EMAIL!
13458
+ const PASSWORD = process.env.SEED_PASSWORD!
13459
+ const NAME = process.env.SEED_NAME || 'Admin'
13460
+
13461
+ async function main() {
13462
+ console.log('\\n Creating admin user...')
13463
+ console.log(\` Email: \${EMAIL}\\n\`)
13464
+
13465
+ const result = await auth.api.signUpEmail({
13466
+ body: { email: EMAIL, password: PASSWORD, name: NAME },
13467
+ })
13468
+
13469
+ if (!result?.user) {
13470
+ console.error(' Failed to create user.')
13471
+ process.exit(1)
12975
13472
  }
12976
- return { isExisting, hasSrcDir, hasTypeScript, hasTailwind, linter, conflicts };
13473
+
13474
+ await db
13475
+ .update(schema.user)
13476
+ .set({ role: 'admin' })
13477
+ .where(eq(schema.user.id, result.user.id))
13478
+
13479
+ console.log(\` Admin user created: \${EMAIL}\`)
13480
+ console.log(' Role: admin\\n')
13481
+ process.exit(0)
12977
13482
  }
12978
- var BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"];
12979
- var ESLINT_CONFIG_FILES = [
12980
- "eslint.config.js",
12981
- "eslint.config.mjs",
12982
- "eslint.config.cjs",
12983
- "eslint.config.ts",
12984
- ".eslintrc.json",
12985
- ".eslintrc.js",
12986
- ".eslintrc.cjs",
12987
- ".eslintrc.yml",
12988
- ".eslintrc.yaml",
12989
- ".eslintrc"
12990
- ];
12991
- function detectLinter(cwd) {
12992
- for (const f of BIOME_CONFIG_FILES) {
12993
- if (fs30.existsSync(path35.join(cwd, f))) {
12994
- return { type: "biome", configFile: f };
12995
- }
13483
+
13484
+ main().catch((err) => {
13485
+ console.error(' Seed failed:', err.message || err)
13486
+ process.exit(1)
13487
+ })
13488
+ `;
13489
+ }
13490
+ var seedCommand = new Command2("seed").description("Create the initial admin user").option("--cwd <path>", "Project root path").action(async (options) => {
13491
+ const cwd = options.cwd ? path36.resolve(options.cwd) : process.cwd();
13492
+ clack.intro("BetterStart Seed");
13493
+ let config;
13494
+ try {
13495
+ config = await resolveConfig(cwd);
13496
+ } catch (err) {
13497
+ clack.cancel(`Error loading config: ${err instanceof Error ? err.message : String(err)}`);
13498
+ process.exit(1);
12996
13499
  }
12997
- for (const f of ESLINT_CONFIG_FILES) {
12998
- if (fs30.existsSync(path35.join(cwd, f))) {
12999
- return { type: "eslint", configFile: f };
13500
+ const cmsDir = config.paths?.cms ?? "./cms";
13501
+ const email = await clack.text({
13502
+ message: "Admin email",
13503
+ placeholder: "admin@example.com",
13504
+ validate: (v) => {
13505
+ if (!v || !v.includes("@")) return "Please enter a valid email";
13000
13506
  }
13507
+ });
13508
+ if (clack.isCancel(email)) {
13509
+ clack.cancel("Cancelled.");
13510
+ process.exit(0);
13001
13511
  }
13002
- const pkgPath = path35.join(cwd, "package.json");
13003
- if (fs30.existsSync(pkgPath)) {
13004
- try {
13005
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
13006
- if (pkg.eslintConfig) {
13007
- return { type: "eslint", configFile: "package.json (eslintConfig)" };
13008
- }
13009
- } catch {
13512
+ const password3 = await clack.password({
13513
+ message: "Admin password",
13514
+ validate: (v) => {
13515
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
13010
13516
  }
13517
+ });
13518
+ if (clack.isCancel(password3)) {
13519
+ clack.cancel("Cancelled.");
13520
+ process.exit(0);
13011
13521
  }
13012
- return { type: "none", configFile: null };
13013
- }
13014
- function detectTailwind(cwd) {
13015
- const cssFiles = ["globals.css", "app.css", "index.css"].flatMap((f) => [
13016
- path35.join(cwd, "src", "app", f),
13017
- path35.join(cwd, "app", f),
13018
- path35.join(cwd, "src", f),
13019
- path35.join(cwd, f)
13020
- ]);
13021
- for (const cssFile of cssFiles) {
13022
- if (fs30.existsSync(cssFile)) {
13023
- const content = fs30.readFileSync(cssFile, "utf-8");
13024
- if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'") || content.includes("@theme")) {
13025
- return true;
13026
- }
13027
- }
13522
+ const name = await clack.text({
13523
+ message: "Admin name",
13524
+ placeholder: "Admin",
13525
+ defaultValue: "Admin"
13526
+ });
13527
+ if (clack.isCancel(name)) {
13528
+ clack.cancel("Cancelled.");
13529
+ process.exit(0);
13028
13530
  }
13029
- const postcssFiles = ["postcss.config.js", "postcss.config.mjs", "postcss.config.cjs"];
13030
- for (const f of postcssFiles) {
13031
- if (fs30.existsSync(path35.join(cwd, f))) {
13032
- const content = fs30.readFileSync(path35.join(cwd, f), "utf-8");
13033
- if (content.includes("tailwindcss") || content.includes("@tailwindcss")) {
13034
- return true;
13035
- }
13036
- }
13531
+ const scriptsDir = path36.join(cwd, cmsDir, "scripts");
13532
+ const seedPath = path36.join(scriptsDir, "seed.ts");
13533
+ if (!fs31.existsSync(scriptsDir)) {
13534
+ fs31.mkdirSync(scriptsDir, { recursive: true });
13037
13535
  }
13038
- return false;
13039
- }
13040
- function hasTsconfigCmsAliases(cwd) {
13041
- const tsconfigPath = path35.join(cwd, "tsconfig.json");
13042
- if (!fs30.existsSync(tsconfigPath)) return false;
13536
+ fs31.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13537
+ const spinner3 = clack.spinner();
13538
+ spinner3.start("Creating admin user...");
13043
13539
  try {
13044
- const content = fs30.readFileSync(tsconfigPath, "utf-8");
13045
- return content.includes("@cms/");
13046
- } catch {
13047
- return false;
13540
+ const { execFileSync: execFileSync5 } = await import("child_process");
13541
+ execFileSync5("npx", ["tsx", seedPath], {
13542
+ cwd,
13543
+ stdio: "pipe",
13544
+ env: {
13545
+ ...process.env,
13546
+ SEED_EMAIL: email,
13547
+ SEED_PASSWORD: password3,
13548
+ SEED_NAME: name || "Admin"
13549
+ }
13550
+ });
13551
+ spinner3.stop("Admin user created");
13552
+ } catch (err) {
13553
+ spinner3.stop("Failed to create admin user");
13554
+ const errMsg = err instanceof Error ? err.message : String(err);
13555
+ clack.log.error(errMsg);
13556
+ clack.log.info("You can run the seed script manually:");
13557
+ clack.log.info(
13558
+ ` SEED_EMAIL="${email}" SEED_PASSWORD="..." npx tsx ${path36.relative(cwd, seedPath)}`
13559
+ );
13560
+ clack.outro("");
13561
+ process.exit(1);
13048
13562
  }
13049
- }
13050
- function hasEnvBetterstartVars(cwd) {
13051
- const envPath = path35.join(cwd, ".env.local");
13052
- if (!fs30.existsSync(envPath)) return false;
13053
13563
  try {
13054
- const content = fs30.readFileSync(envPath, "utf-8");
13055
- return content.includes("BETTERSTART_");
13564
+ fs31.unlinkSync(seedPath);
13565
+ if (fs31.existsSync(scriptsDir) && fs31.readdirSync(scriptsDir).length === 0) {
13566
+ fs31.rmdirSync(scriptsDir);
13567
+ }
13056
13568
  } catch {
13057
- return false;
13058
13569
  }
13059
- }
13570
+ clack.outro(`Admin user ready: ${email}`);
13571
+ });
13060
13572
 
13061
13573
  // src/commands/init.ts
13062
- var initCommand = new Command2("init").description("Scaffold CMS into a new or existing Next.js project").argument("[name]", "Project name (creates new directory if fresh project)").option("--preset <preset>", "Starter preset: blank, blog, or full", "blog").option("-y, --yes", "Skip all prompts (accept defaults)").action(async (name, options) => {
13063
- p3.intro(pc.bgCyan(pc.black(" BetterStart CMS ")));
13064
- let cwd = process.cwd();
13065
- let project = detectProject(cwd);
13066
- let pm = detectPackageManager(cwd);
13067
- p3.log.info(`Package manager: ${pc.cyan(pm)}`);
13068
- let srcDir;
13069
- if (project.isExisting) {
13070
- p3.log.info(`Existing Next.js project detected`);
13071
- srcDir = project.hasSrcDir;
13072
- if (!project.hasTypeScript) {
13073
- p3.log.error("TypeScript is required. Please add a tsconfig.json first.");
13074
- process.exit(1);
13075
- }
13076
- if (project.conflicts.length > 0) {
13077
- p3.log.error("Conflicts detected:");
13078
- for (const conflict of project.conflicts) {
13079
- p3.log.warning(` - ${conflict}`);
13574
+ var initCommand = new Command3("init").description("Scaffold CMS into a new or existing Next.js project").argument("[name]", "Project name (creates new directory if fresh project)").option("--preset <preset>", "Starter preset: blank, blog, or full", "blog").option("-y, --yes", "Skip all prompts (accept defaults)").option(
13575
+ "--database-url <url>",
13576
+ "PostgreSQL database connection string (postgres:// or postgresql://)"
13577
+ ).action(
13578
+ async (name, options) => {
13579
+ p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
13580
+ let cwd = process.cwd();
13581
+ let project = detectProject(cwd);
13582
+ let pm = detectPackageManager(cwd);
13583
+ let srcDir;
13584
+ if (project.isExisting) {
13585
+ p4.log.info(`Existing Next.js project detected`);
13586
+ p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
13587
+ srcDir = project.hasSrcDir;
13588
+ if (!project.hasTypeScript) {
13589
+ p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13590
+ process.exit(1);
13080
13591
  }
13592
+ if (project.conflicts.length > 0) {
13593
+ p4.log.error("Conflicts detected:");
13594
+ for (const conflict of project.conflicts) {
13595
+ p4.log.warning(` - ${conflict}`);
13596
+ }
13597
+ if (!options.yes) {
13598
+ const proceed = await p4.confirm({
13599
+ message: "Continue anyway? (existing files will NOT be overwritten)",
13600
+ initialValue: true
13601
+ });
13602
+ if (p4.isCancel(proceed) || !proceed) {
13603
+ p4.cancel("Setup cancelled.");
13604
+ process.exit(0);
13605
+ }
13606
+ }
13607
+ }
13608
+ } else {
13609
+ p4.log.info("No Next.js project found \u2014 fresh project mode");
13610
+ const projectPrompt = await promptProject(name);
13611
+ srcDir = projectPrompt.useSrcDir;
13081
13612
  if (!options.yes) {
13082
- const proceed = await p3.confirm({
13083
- message: "Continue anyway? (existing files will NOT be overwritten)",
13084
- initialValue: true
13613
+ const pmChoice = await p4.select({
13614
+ message: "Which package manager do you want to use?",
13615
+ options: [
13616
+ { value: "pnpm", label: "pnpm", hint: "recommended" },
13617
+ { value: "npm", label: "npm" },
13618
+ { value: "yarn", label: "yarn" },
13619
+ { value: "bun", label: "bun" }
13620
+ ]
13085
13621
  });
13086
- if (p3.isCancel(proceed) || !proceed) {
13087
- p3.cancel("Setup cancelled.");
13622
+ if (p4.isCancel(pmChoice)) {
13623
+ p4.cancel("Setup cancelled.");
13088
13624
  process.exit(0);
13089
13625
  }
13626
+ pm = pmChoice;
13627
+ }
13628
+ const cnaSpinner = p4.spinner();
13629
+ cnaSpinner.start(`Creating Next.js app: ${projectPrompt.projectName}...`);
13630
+ try {
13631
+ const cnaArgs = [
13632
+ "create-next-app@latest",
13633
+ projectPrompt.projectName,
13634
+ "--typescript",
13635
+ "--tailwind",
13636
+ "--app",
13637
+ "--no-git",
13638
+ "--no-import-alias",
13639
+ "--turbopack",
13640
+ `--use-${pm}`
13641
+ ];
13642
+ if (srcDir) cnaArgs.push("--src-dir");
13643
+ else cnaArgs.push("--no-src-dir");
13644
+ execFileSync4("npx", cnaArgs, {
13645
+ cwd,
13646
+ stdio: "pipe",
13647
+ timeout: 12e4
13648
+ });
13649
+ cnaSpinner.stop(`Created ${projectPrompt.projectName}`);
13650
+ } catch (err) {
13651
+ cnaSpinner.stop("Failed to create Next.js app");
13652
+ p4.log.error(err instanceof Error ? err.message : "create-next-app failed");
13653
+ p4.log.info(
13654
+ `You can create the project manually:
13655
+ ${pc2.cyan(`npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`)}
13656
+ Then run ${pc2.cyan("betterstart init")} inside it.`
13657
+ );
13658
+ process.exit(1);
13090
13659
  }
13660
+ cwd = path37.resolve(cwd, projectPrompt.projectName);
13661
+ project = detectProject(cwd);
13662
+ }
13663
+ const features = options.yes ? { includeEmail: true, preset: options.preset } : await promptFeatures(options.preset);
13664
+ let databaseUrl;
13665
+ const existingDbUrl = readExistingDbUrl(cwd);
13666
+ if (options.yes) {
13667
+ if (options.databaseUrl) {
13668
+ if (!isValidDbUrl(options.databaseUrl)) {
13669
+ p4.log.error(
13670
+ `Invalid database URL. Must start with ${pc2.cyan("postgres://")} or ${pc2.cyan("postgresql://")}`
13671
+ );
13672
+ process.exit(1);
13673
+ }
13674
+ databaseUrl = options.databaseUrl;
13675
+ } else if (existingDbUrl) {
13676
+ databaseUrl = existingDbUrl;
13677
+ }
13678
+ } else if (existingDbUrl) {
13679
+ const masked = maskDbUrl(existingDbUrl);
13680
+ p4.log.info(`Using existing database URL from .env.local ${pc2.dim(`(${masked})`)}`);
13681
+ databaseUrl = existingDbUrl;
13682
+ } else {
13683
+ const dbResult = await promptDatabase();
13684
+ databaseUrl = dbResult.url;
13091
13685
  }
13092
- } else {
13093
- p3.log.info("No Next.js project found \u2014 fresh project mode");
13094
- const projectPrompt = await promptProject(name);
13095
- srcDir = projectPrompt.useSrcDir;
13096
- const cnaSpinner = p3.spinner();
13097
- cnaSpinner.start(`Creating Next.js app: ${projectPrompt.projectName}...`);
13098
- try {
13099
- const cnaArgs = [
13100
- "create-next-app@latest",
13101
- projectPrompt.projectName,
13102
- "--typescript",
13103
- "--tailwind",
13104
- "--app",
13105
- "--no-git",
13106
- "--no-import-alias",
13107
- "--turbopack"
13108
- ];
13109
- if (srcDir) cnaArgs.push("--src-dir");
13110
- else cnaArgs.push("--no-src-dir");
13111
- execFileSync3("npx", cnaArgs, {
13112
- cwd,
13113
- stdio: "pipe",
13114
- timeout: 12e4
13115
- });
13116
- cnaSpinner.stop(`Created ${projectPrompt.projectName}`);
13117
- } catch (err) {
13118
- cnaSpinner.stop("Failed to create Next.js app");
13119
- p3.log.error(err instanceof Error ? err.message : "create-next-app failed");
13120
- p3.log.info(
13121
- `You can create the project manually:
13122
- ${pc.cyan(`npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`)}
13123
- Then run ${pc.cyan("betterstart init")} inside it.`
13686
+ const config = {
13687
+ ...getDefaultConfig(srcDir),
13688
+ features: { email: features.includeEmail }
13689
+ };
13690
+ const s = p4.spinner();
13691
+ s.start("Creating CMS directory structure...");
13692
+ const baseFiles = scaffoldBase({ cwd, config });
13693
+ s.stop(`Created ${baseFiles.length} files`);
13694
+ s.start("Configuring TypeScript path aliases...");
13695
+ const tsResult = scaffoldTsconfig(cwd);
13696
+ s.stop(`Added ${tsResult.added.length} path aliases`);
13697
+ s.start("Configuring Tailwind CSS...");
13698
+ const twResult = scaffoldTailwind(cwd, srcDir);
13699
+ if (twResult.appended) {
13700
+ s.stop(`Updated ${twResult.file}`);
13701
+ } else if (twResult.file) {
13702
+ s.stop("Tailwind already configured for CMS");
13703
+ } else {
13704
+ s.stop("No CSS file found (will configure later)");
13705
+ }
13706
+ s.start("Setting up environment variables...");
13707
+ const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
13708
+ const envParts = [`Added ${envResult.added.length}`];
13709
+ if (envResult.updated.length > 0) envParts.push(`updated ${envResult.updated.length}`);
13710
+ s.stop(`${envParts.join(", ")} env vars in .env.local`);
13711
+ s.start("Setting up database...");
13712
+ const dbFiles = scaffoldDatabase({ cwd, config });
13713
+ s.stop(`Created ${dbFiles.length} database files`);
13714
+ s.start("Setting up authentication...");
13715
+ const authFiles = scaffoldAuth({ cwd, config });
13716
+ s.stop(`Created ${authFiles.length} auth files`);
13717
+ s.start("Copying CMS components...");
13718
+ const compFiles = scaffoldComponents({ cwd, config });
13719
+ s.stop(`Created ${compFiles.length} component files`);
13720
+ s.start("Creating CMS pages and layouts...");
13721
+ const layoutFiles = scaffoldLayout({ cwd, config });
13722
+ s.stop(`Created ${layoutFiles.length} page files`);
13723
+ s.start("Creating API routes...");
13724
+ const apiFiles = scaffoldApiRoutes({ cwd, config });
13725
+ s.stop(`Created ${apiFiles.length} API routes`);
13726
+ s.start("Checking for linter...");
13727
+ if (project.linter.type === "none") {
13728
+ s.stop("No linter found");
13729
+ s.start("Setting up Biome linter...");
13730
+ const biomeResult = scaffoldBiome(cwd, project.linter);
13731
+ if (biomeResult.installed) {
13732
+ s.stop("Created biome.json");
13733
+ } else {
13734
+ s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13735
+ }
13736
+ } else {
13737
+ s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
13738
+ }
13739
+ s.start("Installing dependencies (this may take a minute)...");
13740
+ const depsResult = await installDependenciesAsync({
13741
+ cwd,
13742
+ pm,
13743
+ includeEmail: features.includeEmail,
13744
+ includeBiome: project.linter.type === "none"
13745
+ });
13746
+ if (depsResult.success) {
13747
+ s.stop(
13748
+ `Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`
13749
+ );
13750
+ } else {
13751
+ s.stop("Failed to install dependencies");
13752
+ p4.log.warning(depsResult.error ?? "Unknown error");
13753
+ p4.log.info(
13754
+ `You can install them manually:
13755
+ ${pc2.cyan(`${pm} add ${depsResult.coreDeps.join(" ")}`)}
13756
+ ${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13124
13757
  );
13125
- process.exit(1);
13126
13758
  }
13127
- cwd = path36.resolve(cwd, projectPrompt.projectName);
13128
- project = detectProject(cwd);
13129
- pm = detectPackageManager(cwd);
13130
- }
13131
- const features = options.yes ? { includeEmail: true, preset: options.preset } : await promptFeatures(options.preset);
13132
- const config = {
13133
- ...getDefaultConfig(srcDir),
13134
- features: { email: features.includeEmail }
13135
- };
13136
- const s = p3.spinner();
13137
- s.start("Creating CMS directory structure...");
13138
- const baseFiles = scaffoldBase({ cwd, config });
13139
- s.stop(`Created ${baseFiles.length} files`);
13140
- s.start("Configuring TypeScript path aliases...");
13141
- const tsResult = scaffoldTsconfig(cwd);
13142
- s.stop(`Added ${tsResult.added.length} path aliases`);
13143
- s.start("Configuring Tailwind CSS...");
13144
- const twResult = scaffoldTailwind(cwd, srcDir);
13145
- if (twResult.appended) {
13146
- s.stop(`Updated ${twResult.file}`);
13147
- } else if (twResult.file) {
13148
- s.stop("Tailwind already configured for CMS");
13149
- } else {
13150
- s.stop("No CSS file found (will configure later)");
13151
- }
13152
- s.start("Setting up environment variables...");
13153
- const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail });
13154
- s.stop(`Added ${envResult.added.length} env vars to .env.local`);
13155
- s.start("Setting up database...");
13156
- const dbFiles = scaffoldDatabase({ cwd, config });
13157
- s.stop(`Created ${dbFiles.length} database files`);
13158
- s.start("Setting up authentication...");
13159
- const authFiles = scaffoldAuth({ cwd, config });
13160
- s.stop(`Created ${authFiles.length} auth files`);
13161
- s.start("Copying CMS components...");
13162
- const compFiles = scaffoldComponents({ cwd, config });
13163
- s.stop(`Created ${compFiles.length} component files`);
13164
- s.start("Creating CMS pages and layouts...");
13165
- const layoutFiles = scaffoldLayout({ cwd, config });
13166
- s.stop(`Created ${layoutFiles.length} page files`);
13167
- s.start("Creating API routes...");
13168
- const apiFiles = scaffoldApiRoutes({ cwd, config });
13169
- s.stop(`Created ${apiFiles.length} API routes`);
13170
- s.start("Checking for linter...");
13171
- if (project.linter.type === "none") {
13172
- s.stop("No linter found");
13173
- s.start("Setting up Biome linter...");
13174
- const biomeResult = scaffoldBiome(cwd, project.linter);
13175
- if (biomeResult.installed) {
13176
- s.stop("Created biome.json");
13759
+ s.start(`Applying ${features.preset} preset...`);
13760
+ const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13761
+ if (presetResult.errors.length > 0) {
13762
+ s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13763
+ for (const err of presetResult.errors) {
13764
+ p4.log.warning(` ${err}`);
13765
+ }
13177
13766
  } else {
13178
- s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13767
+ s.stop(
13768
+ `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13769
+ );
13179
13770
  }
13180
- } else {
13181
- s.stop(`Linter: ${pc.cyan(project.linter.type)} (${project.linter.configFile})`);
13182
- }
13183
- s.start("Installing dependencies (this may take a minute)...");
13184
- const depsResult = await installDependenciesAsync({
13185
- cwd,
13186
- pm,
13187
- includeEmail: features.includeEmail,
13188
- includeBiome: project.linter.type === "none"
13189
- });
13190
- if (depsResult.success) {
13191
- s.stop(`Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`);
13192
- } else {
13193
- s.stop("Failed to install dependencies");
13194
- p3.log.warning(depsResult.error ?? "Unknown error");
13195
- p3.log.info(
13196
- `You can install them manually:
13197
- ${pc.cyan(`${pm} add ${depsResult.coreDeps.join(" ")}`)}
13198
- ${pc.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13199
- );
13200
- }
13201
- s.start(`Applying ${features.preset} preset...`);
13202
- const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13203
- if (presetResult.errors.length > 0) {
13204
- s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13205
- for (const err of presetResult.errors) {
13206
- p3.log.warning(` ${err}`);
13771
+ let dbPushed = false;
13772
+ if (depsResult.success && hasDbUrl(cwd)) {
13773
+ s.start("Pushing database schema (drizzle-kit push)...");
13774
+ const pushResult = await runDrizzlePush(cwd);
13775
+ if (pushResult.success) {
13776
+ s.stop("Database schema pushed");
13777
+ dbPushed = true;
13778
+ } else {
13779
+ s.stop("Database push failed");
13780
+ p4.log.warning(pushResult.error ?? "Unknown error");
13781
+ p4.log.info(`You can run it manually: ${pc2.cyan("npx drizzle-kit push")}`);
13782
+ }
13783
+ }
13784
+ let seedEmail;
13785
+ let seedPassword;
13786
+ let seedSuccess = false;
13787
+ if (dbPushed && !options.yes) {
13788
+ p4.log.step("Create your admin account");
13789
+ const email = await p4.text({
13790
+ message: "Admin email",
13791
+ placeholder: "admin@example.com",
13792
+ validate: (v) => {
13793
+ if (!v || !v.includes("@")) return "Please enter a valid email";
13794
+ }
13795
+ });
13796
+ if (p4.isCancel(email)) {
13797
+ p4.cancel("Setup cancelled.");
13798
+ process.exit(0);
13799
+ }
13800
+ const password3 = await p4.password({
13801
+ message: "Admin password",
13802
+ validate: (v) => {
13803
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
13804
+ }
13805
+ });
13806
+ if (p4.isCancel(password3)) {
13807
+ p4.cancel("Setup cancelled.");
13808
+ process.exit(0);
13809
+ }
13810
+ seedEmail = email;
13811
+ seedPassword = password3;
13812
+ s.start("Creating admin user...");
13813
+ const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
13814
+ if (seedResult.success) {
13815
+ s.stop("Admin user created");
13816
+ seedSuccess = true;
13817
+ } else {
13818
+ s.stop("Failed to create admin user");
13819
+ p4.log.warning(seedResult.error ?? "Unknown error");
13820
+ p4.log.info(`You can run it manually: ${pc2.cyan("npx betterstart seed")}`);
13821
+ }
13207
13822
  }
13208
- } else {
13209
- s.stop(
13210
- `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13211
- );
13212
- }
13213
- if (depsResult.success && hasDbUrl(cwd)) {
13214
- s.start("Pushing database schema (drizzle-kit push)...");
13215
- const pushResult = await runDrizzlePush(cwd);
13216
- if (pushResult.success) {
13217
- s.stop("Database schema pushed");
13218
- } else {
13219
- s.stop("Database push failed");
13220
- p3.log.warning(pushResult.error ?? "Unknown error");
13221
- p3.log.info(`You can run it manually: ${pc.cyan("npx drizzle-kit push")}`);
13823
+ {
13824
+ const entityNames = [];
13825
+ const formNames = [];
13826
+ const schemasDir = path37.join(cwd, config.paths.schemas);
13827
+ const formsDir = path37.join(schemasDir, "forms");
13828
+ if (fs32.existsSync(schemasDir)) {
13829
+ for (const f of fs32.readdirSync(schemasDir)) {
13830
+ if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13831
+ }
13832
+ }
13833
+ if (fs32.existsSync(formsDir)) {
13834
+ for (const f of fs32.readdirSync(formsDir)) {
13835
+ if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13836
+ }
13837
+ }
13838
+ regenerateCmsDoc(cwd, config, {
13839
+ preset: features.preset,
13840
+ schemas: entityNames,
13841
+ forms: formNames
13842
+ });
13222
13843
  }
13223
- }
13224
- {
13225
- const entityNames = [];
13226
- const formNames = [];
13227
- const schemasDir = path36.join(cwd, config.paths.schemas);
13228
- const formsDir = path36.join(schemasDir, "forms");
13229
- if (fs31.existsSync(schemasDir)) {
13230
- for (const f of fs31.readdirSync(schemasDir)) {
13231
- if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13844
+ const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
13845
+ const summaryLines = [
13846
+ `Preset: ${pc2.cyan(features.preset)}`,
13847
+ `Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
13848
+ `Files created: ${pc2.cyan(String(totalFiles))}`,
13849
+ `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
13850
+ ];
13851
+ if (seedSuccess && seedEmail && seedPassword) {
13852
+ summaryLines.push(
13853
+ "",
13854
+ `Admin: ${pc2.cyan(seedEmail)}`,
13855
+ `Password: ${pc2.cyan(seedPassword)}`,
13856
+ `CMS: ${pc2.cyan("http://localhost:3000/cms/login")}`
13857
+ );
13858
+ }
13859
+ const nextSteps = [];
13860
+ let step = 1;
13861
+ const envStepLabel = databaseUrl ? `Fill in remaining values in ${pc2.cyan(".env.local")}` : `Fill in values in ${pc2.cyan(".env.local")}`;
13862
+ nextSteps.push(` ${step++}. ${envStepLabel}`);
13863
+ if (!dbPushed) {
13864
+ nextSteps.push(` ${step++}. Run ${pc2.cyan("npx drizzle-kit push")} to sync the database`);
13865
+ }
13866
+ if (!seedSuccess) {
13867
+ nextSteps.push(
13868
+ ` ${step++}. Run ${pc2.cyan("npx betterstart seed")} to create an admin user`
13869
+ );
13870
+ }
13871
+ nextSteps.push(` ${step++}. Run ${pc2.cyan("pnpm run dev")} to start the development server`);
13872
+ nextSteps.push(
13873
+ ` ${step++}. Run ${pc2.cyan("npx betterstart generate <schema>")} to create content types`
13874
+ );
13875
+ summaryLines.push("", "Next steps:", ...nextSteps);
13876
+ p4.note(summaryLines.join("\n"), "CMS scaffolded successfully");
13877
+ if (!options.yes) {
13878
+ const devCmd = runCommand(pm, "dev");
13879
+ const startDev = await p4.confirm({
13880
+ message: "Start the development server?",
13881
+ initialValue: true
13882
+ });
13883
+ if (!p4.isCancel(startDev) && startDev) {
13884
+ p4.outro(`Starting ${pc2.cyan(devCmd)}...`);
13885
+ const [bin, ...args] = devCmd.split(" ");
13886
+ spawn2(bin, args, { cwd, stdio: "inherit" });
13887
+ return;
13232
13888
  }
13233
13889
  }
13234
- if (fs31.existsSync(formsDir)) {
13235
- for (const f of fs31.readdirSync(formsDir)) {
13236
- if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13890
+ p4.outro("Done!");
13891
+ }
13892
+ );
13893
+ function isValidDbUrl(url) {
13894
+ return url.startsWith("postgres://") || url.startsWith("postgresql://");
13895
+ }
13896
+ function readExistingDbUrl(cwd) {
13897
+ const envPath = path37.join(cwd, ".env.local");
13898
+ if (!fs32.existsSync(envPath)) return void 0;
13899
+ const content = fs32.readFileSync(envPath, "utf-8");
13900
+ for (const line of content.split("\n")) {
13901
+ const trimmed = line.trim();
13902
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
13903
+ const [key, ...rest] = trimmed.split("=");
13904
+ if (key?.trim() === "BETTERSTART_DATABASE_URL") {
13905
+ const val = rest.join("=").replace(/^['"]|['"]$/g, "").trim();
13906
+ if (val.length > 0 && !val.startsWith("your_") && val !== "postgresql://..." && isValidDbUrl(val)) {
13907
+ return val;
13237
13908
  }
13238
13909
  }
13239
- regenerateCmsDoc(cwd, config, {
13240
- preset: features.preset,
13241
- schemas: entityNames,
13242
- forms: formNames
13243
- });
13244
13910
  }
13245
- const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
13246
- const dbPushed = depsResult.success && hasDbUrl(cwd);
13247
- const nextSteps = [];
13248
- let step = 1;
13249
- nextSteps.push(` ${step++}. Fill in values in ${pc.cyan(".env.local")}`);
13250
- if (!dbPushed) {
13251
- nextSteps.push(` ${step++}. Run ${pc.cyan("npx drizzle-kit push")} to sync the database`);
13252
- }
13253
- nextSteps.push(` ${step++}. Run ${pc.cyan("npx betterstart seed")} to create an admin user`);
13254
- nextSteps.push(
13255
- ` ${step++}. Run ${pc.cyan("npx betterstart generate <schema>")} to create content types`
13256
- );
13257
- p3.note(
13258
- [
13259
- `Preset: ${pc.cyan(features.preset)}`,
13260
- `Email: ${features.includeEmail ? pc.green("yes") : pc.dim("no")}`,
13261
- `Files created: ${pc.cyan(String(totalFiles))}`,
13262
- `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`,
13263
- "",
13264
- "Next steps:",
13265
- ...nextSteps
13266
- ].join("\n"),
13267
- "CMS scaffolded successfully"
13268
- );
13269
- p3.outro("Done!");
13270
- });
13911
+ return void 0;
13912
+ }
13913
+ function maskDbUrl(url) {
13914
+ try {
13915
+ const parsed = new URL(url);
13916
+ return `${parsed.protocol}//${parsed.host}/***`;
13917
+ } catch {
13918
+ return "postgres://***";
13919
+ }
13920
+ }
13271
13921
  function hasDbUrl(cwd) {
13272
- const envPath = path36.join(cwd, ".env.local");
13273
- if (!fs31.existsSync(envPath)) return false;
13274
- const content = fs31.readFileSync(envPath, "utf-8");
13922
+ const envPath = path37.join(cwd, ".env.local");
13923
+ if (!fs32.existsSync(envPath)) return false;
13924
+ const content = fs32.readFileSync(envPath, "utf-8");
13275
13925
  for (const line of content.split("\n")) {
13276
13926
  const trimmed = line.trim();
13277
13927
  if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
13278
13928
  const [key, ...rest] = trimmed.split("=");
13279
13929
  if (key?.trim() === "BETTERSTART_DATABASE_URL") {
13280
13930
  const val = rest.join("=").trim();
13281
- return val.length > 0 && !val.startsWith("your_") && !val.startsWith('"your_');
13931
+ const unquoted = val.replace(/^['"]|['"]$/g, "");
13932
+ return unquoted.length > 0 && !unquoted.startsWith("your_") && unquoted !== "postgresql://...";
13282
13933
  }
13283
13934
  }
13284
13935
  return false;
13285
13936
  }
13937
+ async function runSeed(cwd, cmsDir, email, password3) {
13938
+ const scriptsDir = path37.join(cwd, cmsDir, "scripts");
13939
+ const seedPath = path37.join(scriptsDir, "seed.ts");
13940
+ if (!fs32.existsSync(scriptsDir)) {
13941
+ fs32.mkdirSync(scriptsDir, { recursive: true });
13942
+ }
13943
+ fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13944
+ try {
13945
+ execFileSync4("npx", ["tsx", seedPath], {
13946
+ cwd,
13947
+ stdio: "pipe",
13948
+ timeout: 3e4,
13949
+ env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
13950
+ });
13951
+ return { success: true, error: null };
13952
+ } catch (err) {
13953
+ const msg = err instanceof Error ? err.message : String(err);
13954
+ return { success: false, error: msg };
13955
+ } finally {
13956
+ try {
13957
+ fs32.unlinkSync(seedPath);
13958
+ if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
13959
+ fs32.rmdirSync(scriptsDir);
13960
+ }
13961
+ } catch {
13962
+ }
13963
+ }
13964
+ }
13286
13965
  function runDrizzlePush(cwd) {
13287
13966
  return new Promise((resolve) => {
13288
13967
  const child = spawn2("npx", ["drizzle-kit", "push", "--force"], {
@@ -13305,16 +13984,16 @@ function runDrizzlePush(cwd) {
13305
13984
  }
13306
13985
 
13307
13986
  // src/commands/remove.ts
13308
- import fs32 from "fs";
13309
- import path37 from "path";
13987
+ import fs33 from "fs";
13988
+ import path38 from "path";
13310
13989
  import readline from "readline";
13311
- import { Command as Command3 } from "commander";
13990
+ import { Command as Command4 } from "commander";
13312
13991
  function toPascalCase17(str) {
13313
13992
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
13314
13993
  }
13315
13994
  function toCamelCase8(str) {
13316
- const p4 = toPascalCase17(str);
13317
- return p4.charAt(0).toLowerCase() + p4.slice(1);
13995
+ const p5 = toPascalCase17(str);
13996
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
13318
13997
  }
13319
13998
  function singularize13(str) {
13320
13999
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -13356,8 +14035,8 @@ function findTableEnd2(content, startIndex) {
13356
14035
  return content.length;
13357
14036
  }
13358
14037
  function removeTableFromSchema(schemaFilePath, name) {
13359
- if (!fs32.existsSync(schemaFilePath)) return false;
13360
- let content = fs32.readFileSync(schemaFilePath, "utf-8");
14038
+ if (!fs33.existsSync(schemaFilePath)) return false;
14039
+ let content = fs33.readFileSync(schemaFilePath, "utf-8");
13361
14040
  const variableName = toCamelCase8(name);
13362
14041
  let changed = false;
13363
14042
  if (content.includes(`export const ${variableName} =`)) {
@@ -13385,13 +14064,13 @@ function removeTableFromSchema(schemaFilePath, name) {
13385
14064
  }
13386
14065
  if (changed) {
13387
14066
  content = content.replace(/\n{3,}/g, "\n\n");
13388
- fs32.writeFileSync(schemaFilePath, content, "utf-8");
14067
+ fs33.writeFileSync(schemaFilePath, content, "utf-8");
13389
14068
  }
13390
14069
  return changed;
13391
14070
  }
13392
14071
  function removeFromNavigation(navFilePath, name) {
13393
- if (!fs32.existsSync(navFilePath)) return false;
13394
- const content = fs32.readFileSync(navFilePath, "utf-8");
14072
+ if (!fs33.existsSync(navFilePath)) return false;
14073
+ const content = fs33.readFileSync(navFilePath, "utf-8");
13395
14074
  const href = `/cms/${name}`;
13396
14075
  if (!content.includes(`'${href}'`)) return false;
13397
14076
  const lines = content.split("\n");
@@ -13422,7 +14101,7 @@ function removeFromNavigation(navFilePath, name) {
13422
14101
  if (startLine === -1 || endLine === -1) return false;
13423
14102
  lines.splice(startLine, endLine - startLine + 1);
13424
14103
  const updated = lines.join("\n").replace(/,\s*,/g, ",").replace(/\[\s*,/, "[");
13425
- fs32.writeFileSync(navFilePath, updated, "utf-8");
14104
+ fs33.writeFileSync(navFilePath, updated, "utf-8");
13426
14105
  return true;
13427
14106
  }
13428
14107
  async function promptConfirm(message) {
@@ -13437,8 +14116,8 @@ async function promptConfirm(message) {
13437
14116
  });
13438
14117
  });
13439
14118
  }
13440
- var removeCommand = new Command3("remove").alias("rm").description("Remove all generated files for an entity or form").argument("<schema>", "Schema name to remove (e.g. posts, categories, contact)").option("-f, --force", "Skip confirmation prompt", false).option("--cwd <path>", "Project root path").action(async (schemaName, options) => {
13441
- const cwd = options.cwd ? path37.resolve(options.cwd) : process.cwd();
14119
+ var removeCommand = new Command4("remove").alias("rm").description("Remove all generated files for an entity or form").argument("<schema>", "Schema name to remove (e.g. posts, categories, contact)").option("-f, --force", "Skip confirmation prompt", false).option("--cwd <path>", "Project root path").action(async (schemaName, options) => {
14120
+ const cwd = options.cwd ? path38.resolve(options.cwd) : process.cwd();
13442
14121
  console.log("\n BetterStart Remove\n");
13443
14122
  let config;
13444
14123
  try {
@@ -13451,34 +14130,34 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13451
14130
  const pagesDir = config.paths?.pages ?? "./src/app/(cms)/cms/(authenticated)";
13452
14131
  const kebabName = toKebabCase9(schemaName);
13453
14132
  const targets = [];
13454
- const entityPagesDir = path37.join(cwd, pagesDir, schemaName);
13455
- if (fs32.existsSync(entityPagesDir)) {
14133
+ const entityPagesDir = path38.join(cwd, pagesDir, schemaName);
14134
+ if (fs33.existsSync(entityPagesDir)) {
13456
14135
  targets.push({
13457
14136
  path: entityPagesDir,
13458
- label: `${path37.join(pagesDir, schemaName)}/`,
14137
+ label: `${path38.join(pagesDir, schemaName)}/`,
13459
14138
  isDir: true
13460
14139
  });
13461
14140
  }
13462
- const actionsFile = path37.join(cwd, cmsDir, "lib", "actions", `${kebabName}.ts`);
13463
- if (fs32.existsSync(actionsFile)) {
14141
+ const actionsFile = path38.join(cwd, cmsDir, "lib", "actions", `${kebabName}.ts`);
14142
+ if (fs33.existsSync(actionsFile)) {
13464
14143
  targets.push({
13465
14144
  path: actionsFile,
13466
- label: path37.join(cmsDir, "lib", "actions", `${kebabName}.ts`),
14145
+ label: path38.join(cmsDir, "lib", "actions", `${kebabName}.ts`),
13467
14146
  isDir: false
13468
14147
  });
13469
14148
  }
13470
- const hookFile = path37.join(cwd, cmsDir, "hooks", `use-${kebabName}.ts`);
13471
- if (fs32.existsSync(hookFile)) {
14149
+ const hookFile = path38.join(cwd, cmsDir, "hooks", `use-${kebabName}.ts`);
14150
+ if (fs33.existsSync(hookFile)) {
13472
14151
  targets.push({
13473
14152
  path: hookFile,
13474
- label: path37.join(cmsDir, "hooks", `use-${kebabName}.ts`),
14153
+ label: path38.join(cmsDir, "hooks", `use-${kebabName}.ts`),
13475
14154
  isDir: false
13476
14155
  });
13477
14156
  }
13478
- const schemaFilePath = path37.join(cwd, cmsDir, "db", "schema.ts");
13479
- const hasTable = fs32.existsSync(schemaFilePath) && fs32.readFileSync(schemaFilePath, "utf-8").includes(`export const ${toCamelCase8(schemaName)} =`);
13480
- const navFilePath = path37.join(cwd, cmsDir, "data", "navigation.ts");
13481
- const hasNavEntry = fs32.existsSync(navFilePath) && fs32.readFileSync(navFilePath, "utf-8").includes(`'/cms/${schemaName}'`);
14157
+ const schemaFilePath = path38.join(cwd, cmsDir, "db", "schema.ts");
14158
+ const hasTable = fs33.existsSync(schemaFilePath) && fs33.readFileSync(schemaFilePath, "utf-8").includes(`export const ${toCamelCase8(schemaName)} =`);
14159
+ const navFilePath = path38.join(cwd, cmsDir, "data", "navigation.ts");
14160
+ const hasNavEntry = fs33.existsSync(navFilePath) && fs33.readFileSync(navFilePath, "utf-8").includes(`'/cms/${schemaName}'`);
13482
14161
  if (targets.length === 0 && !hasTable && !hasNavEntry) {
13483
14162
  console.log(` No generated files found for: ${schemaName}`);
13484
14163
  return;
@@ -13488,10 +14167,10 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13488
14167
  console.log(` ${t.isDir ? "[dir]" : " "} ${t.label}`);
13489
14168
  }
13490
14169
  if (hasTable) {
13491
- console.log(` [edit] ${path37.join(cmsDir, "db", "schema.ts")} (remove table)`);
14170
+ console.log(` [edit] ${path38.join(cmsDir, "db", "schema.ts")} (remove table)`);
13492
14171
  }
13493
14172
  if (hasNavEntry) {
13494
- console.log(` [edit] ${path37.join(cmsDir, "data", "navigation.ts")} (remove entry)`);
14173
+ console.log(` [edit] ${path38.join(cmsDir, "data", "navigation.ts")} (remove entry)`);
13495
14174
  }
13496
14175
  if (!options.force) {
13497
14176
  console.log("");
@@ -13504,19 +14183,19 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13504
14183
  console.log("");
13505
14184
  for (const t of targets) {
13506
14185
  if (t.isDir) {
13507
- fs32.rmSync(t.path, { recursive: true, force: true });
14186
+ fs33.rmSync(t.path, { recursive: true, force: true });
13508
14187
  } else {
13509
- fs32.unlinkSync(t.path);
14188
+ fs33.unlinkSync(t.path);
13510
14189
  }
13511
14190
  console.log(` Removed: ${t.label}`);
13512
14191
  }
13513
14192
  if (hasTable) {
13514
14193
  removeTableFromSchema(schemaFilePath, schemaName);
13515
- console.log(` Cleaned: ${path37.join(cmsDir, "db", "schema.ts")}`);
14194
+ console.log(` Cleaned: ${path38.join(cmsDir, "db", "schema.ts")}`);
13516
14195
  }
13517
14196
  if (hasNavEntry) {
13518
14197
  removeFromNavigation(navFilePath, schemaName);
13519
- console.log(` Cleaned: ${path37.join(cmsDir, "data", "navigation.ts")}`);
14198
+ console.log(` Cleaned: ${path38.join(cmsDir, "data", "navigation.ts")}`);
13520
14199
  }
13521
14200
  console.log("\n Removal complete!");
13522
14201
  console.log("\n Note: You may need to manually:");
@@ -13526,170 +14205,6 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13526
14205
  console.log("");
13527
14206
  });
13528
14207
 
13529
- // src/commands/seed.ts
13530
- import fs33 from "fs";
13531
- import path38 from "path";
13532
- import * as clack from "@clack/prompts";
13533
- import { Command as Command4 } from "commander";
13534
- function buildSeedScript() {
13535
- return `/**
13536
- * BetterStart CMS \u2014 Seed Script
13537
- * Creates the initial admin user
13538
- * AUTO-GENERATED \u2014 safe to delete after running
13539
- */
13540
-
13541
- import { loadEnvConfig } from '@next/env'
13542
- loadEnvConfig(process.cwd())
13543
-
13544
- import { neon } from '@neondatabase/serverless'
13545
- import { drizzle } from 'drizzle-orm/neon-http'
13546
- import { eq } from 'drizzle-orm'
13547
- import * as schema from '../db/schema'
13548
- import { betterAuth } from 'better-auth'
13549
- import { drizzleAdapter } from 'better-auth/adapters/drizzle'
13550
-
13551
- // Inline DB connection (mirrors cms/db/client.ts)
13552
- const sql = neon(process.env.BETTERSTART_DATABASE_URL!)
13553
- const db = drizzle({ client: sql, schema })
13554
-
13555
- // Inline auth setup (mirrors cms/lib/auth/auth.ts)
13556
- const auth = betterAuth({
13557
- secret: process.env.BETTERSTART_AUTH_SECRET,
13558
- baseURL: process.env.BETTERSTART_AUTH_URL,
13559
- basePath: process.env.BETTERSTART_AUTH_BASE_PATH || '/api/cms/auth',
13560
- database: drizzleAdapter(db, {
13561
- provider: 'pg',
13562
- schema: {
13563
- user: schema.user,
13564
- session: schema.session,
13565
- account: schema.account,
13566
- verification: schema.verification,
13567
- },
13568
- }),
13569
- emailAndPassword: { enabled: true, minPasswordLength: 8 },
13570
- user: {
13571
- additionalFields: {
13572
- role: { type: 'string', required: false, defaultValue: 'member', input: false },
13573
- },
13574
- },
13575
- })
13576
-
13577
- const EMAIL = process.env.SEED_EMAIL!
13578
- const PASSWORD = process.env.SEED_PASSWORD!
13579
- const NAME = process.env.SEED_NAME || 'Admin'
13580
-
13581
- async function main() {
13582
- console.log('\\n Creating admin user...')
13583
- console.log(\` Email: \${EMAIL}\\n\`)
13584
-
13585
- const result = await auth.api.signUpEmail({
13586
- body: { email: EMAIL, password: PASSWORD, name: NAME },
13587
- })
13588
-
13589
- if (!result?.user) {
13590
- console.error(' Failed to create user.')
13591
- process.exit(1)
13592
- }
13593
-
13594
- await db
13595
- .update(schema.user)
13596
- .set({ role: 'admin' })
13597
- .where(eq(schema.user.id, result.user.id))
13598
-
13599
- console.log(\` Admin user created: \${EMAIL}\`)
13600
- console.log(' Role: admin\\n')
13601
- process.exit(0)
13602
- }
13603
-
13604
- main().catch((err) => {
13605
- console.error(' Seed failed:', err.message || err)
13606
- process.exit(1)
13607
- })
13608
- `;
13609
- }
13610
- var seedCommand = new Command4("seed").description("Create the initial admin user").option("--cwd <path>", "Project root path").action(async (options) => {
13611
- const cwd = options.cwd ? path38.resolve(options.cwd) : process.cwd();
13612
- clack.intro("BetterStart Seed");
13613
- let config;
13614
- try {
13615
- config = await resolveConfig(cwd);
13616
- } catch (err) {
13617
- clack.cancel(`Error loading config: ${err instanceof Error ? err.message : String(err)}`);
13618
- process.exit(1);
13619
- }
13620
- const cmsDir = config.paths?.cms ?? "./cms";
13621
- const email = await clack.text({
13622
- message: "Admin email",
13623
- placeholder: "admin@example.com",
13624
- validate: (v) => {
13625
- if (!v || !v.includes("@")) return "Please enter a valid email";
13626
- }
13627
- });
13628
- if (clack.isCancel(email)) {
13629
- clack.cancel("Cancelled.");
13630
- process.exit(0);
13631
- }
13632
- const password2 = await clack.password({
13633
- message: "Admin password",
13634
- validate: (v) => {
13635
- if (!v || v.length < 8) return "Password must be at least 8 characters";
13636
- }
13637
- });
13638
- if (clack.isCancel(password2)) {
13639
- clack.cancel("Cancelled.");
13640
- process.exit(0);
13641
- }
13642
- const name = await clack.text({
13643
- message: "Admin name",
13644
- placeholder: "Admin",
13645
- defaultValue: "Admin"
13646
- });
13647
- if (clack.isCancel(name)) {
13648
- clack.cancel("Cancelled.");
13649
- process.exit(0);
13650
- }
13651
- const scriptsDir = path38.join(cwd, cmsDir, "scripts");
13652
- const seedPath = path38.join(scriptsDir, "seed.ts");
13653
- if (!fs33.existsSync(scriptsDir)) {
13654
- fs33.mkdirSync(scriptsDir, { recursive: true });
13655
- }
13656
- fs33.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13657
- const spinner3 = clack.spinner();
13658
- spinner3.start("Creating admin user...");
13659
- try {
13660
- const { execFileSync: execFileSync4 } = await import("child_process");
13661
- execFileSync4("npx", ["tsx", seedPath], {
13662
- cwd,
13663
- stdio: "pipe",
13664
- env: {
13665
- ...process.env,
13666
- SEED_EMAIL: email,
13667
- SEED_PASSWORD: password2,
13668
- SEED_NAME: name || "Admin"
13669
- }
13670
- });
13671
- spinner3.stop("Admin user created");
13672
- } catch (err) {
13673
- spinner3.stop("Failed to create admin user");
13674
- const errMsg = err instanceof Error ? err.message : String(err);
13675
- clack.log.error(errMsg);
13676
- clack.log.info("You can run the seed script manually:");
13677
- clack.log.info(
13678
- ` SEED_EMAIL="${email}" SEED_PASSWORD="..." npx tsx ${path38.relative(cwd, seedPath)}`
13679
- );
13680
- clack.outro("");
13681
- process.exit(1);
13682
- }
13683
- try {
13684
- fs33.unlinkSync(seedPath);
13685
- if (fs33.existsSync(scriptsDir) && fs33.readdirSync(scriptsDir).length === 0) {
13686
- fs33.rmdirSync(scriptsDir);
13687
- }
13688
- } catch {
13689
- }
13690
- clack.outro(`Admin user ready: ${email}`);
13691
- });
13692
-
13693
14208
  // src/cli.ts
13694
14209
  var program = new Command5();
13695
14210
  program.name("betterstart").description("Scaffold a full-featured CMS into any Next.js 16 application").version("0.1.0");