@betterstart/cli 0.1.5 → 0.1.7

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,30 +9263,34 @@ export function ReorderControls({
8986
9263
  function cmsHeaderTemplate() {
8987
9264
  return `'use client'
8988
9265
 
8989
- import { Moon, Sun } from 'lucide-react'
8990
- import { useTheme } from '@cms/hooks/use-cms-theme'
9266
+ import { CmsSearch } from '@cms/components/layout/cms-search'
8991
9267
  import { Button } from '@cms/components/ui/button'
9268
+ import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
9269
+ import { useTheme } from '@cms/hooks/use-cms-theme'
9270
+ import { Moon, Sun } from 'lucide-react'
8992
9271
 
8993
9272
  export function CmsHeader() {
8994
9273
  const { theme, setTheme } = useTheme()
9274
+ const { state } = useSidebar()
8995
9275
 
8996
9276
  return (
8997
- <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border justify-between w-full sticky top-0 z-50 bg-background">
8998
- <div className="flex items-center gap-3 px-5 w-full">
8999
- <span className="text-sm font-medium text-muted-foreground block md:hidden">
9000
- CMS
9001
- </span>
9002
- </div>
9003
- <div className="flex items-center px-5 gap-1">
9004
- <Button
9005
- variant="ghost"
9006
- size="icon"
9007
- onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
9008
- >
9009
- <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
9010
- <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
9011
- <span className="sr-only">Toggle theme</span>
9012
- </Button>
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">
9278
+ <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
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>
9013
9294
  </div>
9014
9295
  </header>
9015
9296
  )
@@ -9055,9 +9336,40 @@ export function CmsProviders({ children }: { children: React.ReactNode }) {
9055
9336
  `;
9056
9337
  }
9057
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
+
9058
9369
  // src/init/templates/components/layout/cms-sidebar.ts
9059
9370
  function cmsSidebarTemplate() {
9060
- return `import { getSession } from '@cms/auth/middleware'
9371
+ return `import { getSetting } from '@/cms/lib/actions/settings'
9372
+ import { getSession } from '@cms/auth/middleware'
9061
9373
  import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
9062
9374
  import {
9063
9375
  Collapsible,
@@ -9078,20 +9390,21 @@ import {
9078
9390
  SidebarRail,
9079
9391
  SidebarTrigger,
9080
9392
  } from '@cms/components/ui/sidebar'
9393
+ import { cms } from '@cms/data/cms'
9081
9394
  import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
9082
- import { ChevronRight } from 'lucide-react'
9395
+ import { ChevronRight, Settings, Users } from 'lucide-react'
9083
9396
  import Link from 'next/link'
9084
9397
 
9085
9398
  function NavItem({ item }: { item: CmsNavigationItem }) {
9086
9399
  if (item.children && item.children.length > 0) {
9087
9400
  return (
9088
- <Collapsible asChild defaultOpen className="group/collapsible">
9401
+ <Collapsible asChild defaultOpen className="group/collapsible border-y border-border py-2 px-2">
9089
9402
  <SidebarMenuItem>
9090
9403
  <CollapsibleTrigger asChild>
9091
9404
  <SidebarMenuButton>
9092
- {item.icon && <item.icon className="h-4 w-4" />}
9405
+ {item.icon && <item.icon className="size-3.5!" />}
9093
9406
  <span>{item.label}</span>
9094
- <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" />
9095
9408
  </SidebarMenuButton>
9096
9409
  </CollapsibleTrigger>
9097
9410
  <CollapsibleContent>
@@ -9100,7 +9413,7 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9100
9413
  <SidebarMenuSubItem key={child.href}>
9101
9414
  <SidebarMenuSubButton asChild>
9102
9415
  <Link href={child.href}>
9103
- {child.icon && <child.icon className="h-4 w-4" />}
9416
+ {child.icon && <child.icon className="size-3.5!" />}
9104
9417
  <span>{child.label}</span>
9105
9418
  </Link>
9106
9419
  </SidebarMenuSubButton>
@@ -9114,10 +9427,10 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9114
9427
  }
9115
9428
 
9116
9429
  return (
9117
- <SidebarMenuItem>
9430
+ <SidebarMenuItem className="px-2">
9118
9431
  <SidebarMenuButton asChild>
9119
9432
  <Link href={item.href}>
9120
- {item.icon && <item.icon className="h-4 w-4" />}
9433
+ {item.icon && <item.icon className="size-3.5!" />}
9121
9434
  <span>{item.label}</span>
9122
9435
  </Link>
9123
9436
  </SidebarMenuButton>
@@ -9127,32 +9440,51 @@ function NavItem({ item }: { item: CmsNavigationItem }) {
9127
9440
 
9128
9441
  export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9129
9442
  const session = await getSession()
9443
+ const settings = await getSetting()
9130
9444
  const user = session?.user ?? null
9131
9445
 
9132
9446
  return (
9133
9447
  <Sidebar collapsible="icon" {...props}>
9134
9448
  <SidebarHeader className="border-b border-border h-14 items-center flex w-full">
9135
9449
  <div className="flex items-center gap-2 w-full relative h-full">
9136
- <div className="flex items-center gap-2 w-full">
9137
- <Avatar className="size-8">
9138
- <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'} />
9139
9453
  <AvatarFallback className="text-sm font-semibold">
9140
- {user?.name?.charAt(0)}
9454
+ {settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
9141
9455
  </AvatarFallback>
9142
9456
  </Avatar>
9143
9457
  <div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9144
- <span className="text-base font-bold">CMS</span>
9458
+ <span className="text-sm font-semibold line-clamp-1">{settings?.siteName ?? cms.name}</span>
9145
9459
  </div>
9146
- </div>
9460
+ </Link>
9147
9461
  <SidebarTrigger className="hidden md:flex" />
9148
9462
  </div>
9149
9463
  </SidebarHeader>
9150
- <SidebarContent>
9151
- <SidebarMenu className="p-2 gap-1">
9464
+ <SidebarContent className="gap-2">
9465
+ <SidebarMenu className="py-2 gap-2">
9152
9466
  {cmsNavigation.map((item) => (
9153
9467
  <NavItem key={item.href + item.label} item={item} />
9154
9468
  ))}
9155
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>
9156
9488
  </SidebarContent>
9157
9489
  <SidebarFooter>
9158
9490
  {user && (
@@ -9317,9 +9649,17 @@ export function BooleanBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }: {
9317
9649
  `;
9318
9650
  }
9319
9651
 
9652
+ // src/init/templates/data/cms.ts
9653
+ function cmsDataTemplate(projectName) {
9654
+ return `export const cms = {
9655
+ name: '${projectName}',
9656
+ }
9657
+ `;
9658
+ }
9659
+
9320
9660
  // src/init/templates/data/navigation.ts
9321
9661
  function navigationDataTemplate() {
9322
- return `import { House, Settings, Users } from 'lucide-react'
9662
+ return `import { House } from 'lucide-react'
9323
9663
  import type { LucideIcon } from 'lucide-react'
9324
9664
 
9325
9665
  export interface CmsNavigationItem {
@@ -9334,16 +9674,6 @@ export const cmsNavigation: CmsNavigationItem[] = [
9334
9674
  label: 'Dashboard',
9335
9675
  href: '/cms',
9336
9676
  icon: House
9337
- },
9338
- {
9339
- label: 'Users',
9340
- href: '/cms/users',
9341
- icon: Users
9342
- },
9343
- {
9344
- label: 'Settings',
9345
- href: '/cms/settings',
9346
- icon: Settings
9347
9677
  }
9348
9678
  ]
9349
9679
  `;
@@ -10116,6 +10446,11 @@ export interface UpdateUserRoleResult {
10116
10446
  error?: string
10117
10447
  }
10118
10448
 
10449
+ export interface DeleteUserResult {
10450
+ success: boolean
10451
+ error?: string
10452
+ }
10453
+
10119
10454
  /**
10120
10455
  * Create a new user via Better Auth's built-in API
10121
10456
  */
@@ -10208,6 +10543,21 @@ export async function updateUserRole(
10208
10543
  }
10209
10544
  }
10210
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
+ }
10211
10561
  `;
10212
10562
  }
10213
10563
 
@@ -10928,18 +11278,19 @@ export function sendWebhook(
10928
11278
 
10929
11279
  // src/init/scaffolders/components.ts
10930
11280
  function scaffoldComponents({ cwd, config }) {
10931
- const cms = path28.resolve(cwd, config.paths.cms);
11281
+ const cms = path29.resolve(cwd, config.paths.cms);
10932
11282
  const created = [];
10933
11283
  function write(relPath, content) {
10934
- const fullPath = path28.join(cms, relPath);
11284
+ const fullPath = path29.join(cms, relPath);
10935
11285
  if (safeWriteFile(fullPath, content)) {
10936
- created.push(path28.join(config.paths.cms, relPath));
11286
+ created.push(path29.join(config.paths.cms, relPath));
10937
11287
  }
10938
11288
  }
10939
11289
  write("cms-globals.css", cmsGlobalsCssTemplate());
10940
11290
  write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
10941
11291
  write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
10942
11292
  write("components/layout/cms-header.tsx", cmsHeaderTemplate());
11293
+ write("components/layout/cms-search.tsx", cmsSearchTemplate());
10943
11294
  write("components/shared/page-header.tsx", pageHeaderTemplate());
10944
11295
  write("components/shared/delete-dialog.tsx", deleteDialogTemplate());
10945
11296
  write("components/shared/status-badge.tsx", statusBadgeTemplate());
@@ -10962,6 +11313,8 @@ function scaffoldComponents({ cwd, config }) {
10962
11313
  write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
10963
11314
  write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
10964
11315
  write("hooks/use-users.ts", useUsersHookTemplate());
11316
+ const projectName = detectProjectName(cwd);
11317
+ write("data/cms.ts", cmsDataTemplate(projectName));
10965
11318
  write("data/navigation.ts", navigationDataTemplate());
10966
11319
  write("lib/r2.ts", r2ClientTemplate());
10967
11320
  write("lib/actions/form-settings.ts", formSettingsActionTemplate());
@@ -10976,18 +11329,18 @@ function scaffoldComponents({ cwd, config }) {
10976
11329
  }
10977
11330
  function copyUiTemplates(cwd, config) {
10978
11331
  const created = [];
10979
- const destDir = path28.resolve(cwd, config.paths.cms, "components", "ui");
11332
+ const destDir = path29.resolve(cwd, config.paths.cms, "components", "ui");
10980
11333
  const cliRoot = findCliRoot();
10981
- const srcDir = path28.join(cliRoot, "templates", "ui");
10982
- if (!fs25.existsSync(srcDir)) {
11334
+ const srcDir = path29.join(cliRoot, "templates", "ui");
11335
+ if (!fs26.existsSync(srcDir)) {
10983
11336
  return created;
10984
11337
  }
10985
- 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"));
10986
11339
  for (const file of files) {
10987
- const destPath = path28.join(destDir, file);
10988
- if (!fs25.existsSync(destPath)) {
10989
- fs25.copyFileSync(path28.join(srcDir, file), destPath);
10990
- 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));
10991
11344
  }
10992
11345
  }
10993
11346
  return created;
@@ -10995,25 +11348,25 @@ function copyUiTemplates(cwd, config) {
10995
11348
  function copyTiptapTemplates(cwd, config) {
10996
11349
  const created = [];
10997
11350
  const cliRoot = findCliRoot();
10998
- const srcDir = path28.join(cliRoot, "templates", "tiptap");
10999
- const destDir = path28.resolve(cwd, config.paths.cms, "components", "ui", "tiptap");
11000
- 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)) {
11001
11354
  return created;
11002
11355
  }
11003
11356
  copyDirRecursive(srcDir, destDir, config.paths.cms, created);
11004
11357
  return created;
11005
11358
  }
11006
11359
  function copyDirRecursive(src, dest, cmsPrefix, created) {
11007
- fs25.ensureDirSync(dest);
11008
- const entries = fs25.readdirSync(src, { withFileTypes: true });
11360
+ fs26.ensureDirSync(dest);
11361
+ const entries = fs26.readdirSync(src, { withFileTypes: true });
11009
11362
  for (const entry of entries) {
11010
- const srcPath = path28.join(src, entry.name);
11011
- const destPath = path28.join(dest, entry.name);
11363
+ const srcPath = path29.join(src, entry.name);
11364
+ const destPath = path29.join(dest, entry.name);
11012
11365
  if (entry.isDirectory()) {
11013
11366
  copyDirRecursive(srcPath, destPath, cmsPrefix, created);
11014
- } else if (!fs25.existsSync(destPath)) {
11015
- fs25.copyFileSync(srcPath, destPath);
11016
- 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);
11017
11370
  created.push(relFromCms);
11018
11371
  }
11019
11372
  }
@@ -11021,35 +11374,35 @@ function copyDirRecursive(src, dest, cmsPrefix, created) {
11021
11374
  function copySchemaMetaschema(cwd, config) {
11022
11375
  const created = [];
11023
11376
  const cliRoot = findCliRoot();
11024
- const srcPath = path28.join(cliRoot, "templates", "schema.json");
11025
- const destPath = path28.resolve(cwd, config.paths.schemas, "schema.json");
11026
- if (fs25.existsSync(srcPath) && !fs25.existsSync(destPath)) {
11027
- fs25.ensureDirSync(path28.dirname(destPath));
11028
- fs25.copyFileSync(srcPath, destPath);
11029
- 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"));
11030
11383
  }
11031
11384
  return created;
11032
11385
  }
11033
11386
  function findCliRoot() {
11034
11387
  let dir = new URL(".", import.meta.url).pathname;
11035
11388
  for (let i = 0; i < 5; i++) {
11036
- const pkgPath = path28.join(dir, "package.json");
11037
- if (fs25.existsSync(pkgPath)) {
11389
+ const pkgPath = path29.join(dir, "package.json");
11390
+ if (fs26.existsSync(pkgPath)) {
11038
11391
  try {
11039
- const pkg = JSON.parse(fs25.readFileSync(pkgPath, "utf-8"));
11392
+ const pkg = JSON.parse(fs26.readFileSync(pkgPath, "utf-8"));
11040
11393
  if (pkg.name === "@betterstart/cli") {
11041
11394
  return dir;
11042
11395
  }
11043
11396
  } catch {
11044
11397
  }
11045
11398
  }
11046
- dir = path28.dirname(dir);
11399
+ dir = path29.dirname(dir);
11047
11400
  }
11048
- return path28.resolve(new URL(".", import.meta.url).pathname, "..", "..");
11401
+ return path29.resolve(new URL(".", import.meta.url).pathname, "..", "..");
11049
11402
  }
11050
11403
 
11051
11404
  // src/init/scaffolders/database.ts
11052
- import path29 from "path";
11405
+ import path30 from "path";
11053
11406
 
11054
11407
  // src/init/templates/db/client.ts
11055
11408
  function dbClientTemplate() {
@@ -11178,16 +11531,16 @@ export const formSettings = pgTable(
11178
11531
  // src/init/scaffolders/database.ts
11179
11532
  function scaffoldDatabase({ cwd, config }) {
11180
11533
  const created = [];
11181
- const dbDir = path29.resolve(cwd, config.paths.cms, "db");
11534
+ const dbDir = path30.resolve(cwd, config.paths.cms, "db");
11182
11535
  function write(filename, content) {
11183
- const fullPath = path29.join(dbDir, filename);
11536
+ const fullPath = path30.join(dbDir, filename);
11184
11537
  if (safeWriteFile(fullPath, content)) {
11185
- created.push(path29.join(config.paths.cms, "db", filename));
11538
+ created.push(path30.join(config.paths.cms, "db", filename));
11186
11539
  }
11187
11540
  }
11188
11541
  write("client.ts", dbClientTemplate());
11189
11542
  write("schema.ts", dbSchemaTemplate());
11190
- const drizzleConfigPath = path29.resolve(cwd, "drizzle.config.ts");
11543
+ const drizzleConfigPath = path30.resolve(cwd, "drizzle.config.ts");
11191
11544
  if (safeWriteFile(drizzleConfigPath, drizzleConfigTemplate())) {
11192
11545
  created.push("drizzle.config.ts");
11193
11546
  }
@@ -11350,25 +11703,41 @@ async function installDependenciesAsync({
11350
11703
  }
11351
11704
  }
11352
11705
 
11706
+ // src/init/scaffolders/env.ts
11707
+ import crypto from "crypto";
11708
+
11353
11709
  // src/utils/env.ts
11354
- import fs26 from "fs";
11355
- import path30 from "path";
11356
- function appendEnvVars(cwd, sections) {
11357
- const envPath = path30.join(cwd, ".env.local");
11358
- 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") : "";
11359
11715
  const existingKeys = new Set(
11360
11716
  existing.split("\n").filter((line) => line.trim() && !line.trim().startsWith("#")).map((line) => line.split("=")[0]?.trim()).filter(Boolean)
11361
11717
  );
11362
11718
  const added = [];
11363
11719
  const skipped = [];
11720
+ const updated = [];
11364
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
+ }
11365
11734
  if (existing.trim()) {
11366
11735
  lines.push("");
11367
11736
  }
11368
11737
  for (const section of sections) {
11369
11738
  const sectionVars = section.vars.filter((v) => {
11370
11739
  if (existingKeys.has(v.key)) {
11371
- skipped.push(v.key);
11740
+ if (!updated.includes(v.key)) skipped.push(v.key);
11372
11741
  return false;
11373
11742
  }
11374
11743
  added.push(v.key);
@@ -11382,26 +11751,27 @@ function appendEnvVars(cwd, sections) {
11382
11751
  }
11383
11752
  lines.push("");
11384
11753
  }
11385
- if (added.length > 0) {
11754
+ if (added.length > 0 || updated.length > 0) {
11386
11755
  const header = existing.trim() ? "" : "# ============================================\n# BetterStart CMS\n# ============================================\n";
11387
11756
  const content = existing.trim() ? `${existing.trimEnd()}
11388
11757
  ${lines.join("\n")}` : header + lines.join("\n");
11389
- fs26.writeFileSync(envPath, content);
11758
+ fs27.writeFileSync(envPath, content);
11390
11759
  }
11391
- return { added, skipped };
11760
+ return { added, skipped, updated };
11392
11761
  }
11393
11762
 
11394
11763
  // src/init/scaffolders/env.ts
11395
- function getCoreEnvSections() {
11764
+ function getCoreEnvSections(databaseUrl) {
11765
+ const authSecret = crypto.randomBytes(32).toString("base64");
11396
11766
  return [
11397
11767
  {
11398
11768
  header: "Database (Neon)",
11399
- vars: [{ key: "BETTERSTART_DATABASE_URL", value: "postgresql://..." }]
11769
+ vars: [{ key: "BETTERSTART_DATABASE_URL", value: databaseUrl ?? "postgresql://..." }]
11400
11770
  },
11401
11771
  {
11402
11772
  header: "Authentication",
11403
11773
  vars: [
11404
- { key: "BETTERSTART_AUTH_SECRET", value: "", comment: "openssl rand -base64 32" },
11774
+ { key: "BETTERSTART_AUTH_SECRET", value: authSecret },
11405
11775
  { key: "BETTERSTART_AUTH_URL", value: "http://localhost:3000" },
11406
11776
  { key: "BETTERSTART_AUTH_BASE_PATH", value: "/api/cms/auth" }
11407
11777
  ]
@@ -11428,15 +11798,16 @@ function getEmailEnvSection() {
11428
11798
  };
11429
11799
  }
11430
11800
  function scaffoldEnv(cwd, options) {
11431
- const sections = getCoreEnvSections();
11801
+ const sections = getCoreEnvSections(options.databaseUrl);
11432
11802
  if (options.includeEmail) {
11433
11803
  sections.push(getEmailEnvSection());
11434
11804
  }
11435
- return appendEnvVars(cwd, sections);
11805
+ const overwrite = options.databaseUrl ? /* @__PURE__ */ new Set(["BETTERSTART_DATABASE_URL"]) : void 0;
11806
+ return appendEnvVars(cwd, sections, overwrite);
11436
11807
  }
11437
11808
 
11438
11809
  // src/init/scaffolders/layout.ts
11439
- import path31 from "path";
11810
+ import path32 from "path";
11440
11811
 
11441
11812
  // src/init/templates/pages/authenticated-layout.ts
11442
11813
  function authenticatedLayoutTemplate() {
@@ -11932,10 +12303,22 @@ export function EditRoleDialog({
11932
12303
  function usersColumnsTemplate() {
11933
12304
  return `'use client'
11934
12305
 
12306
+ import React from 'react'
11935
12307
  import {
11936
12308
  Avatar,
11937
12309
  AvatarFallback
11938
12310
  } from '@cms/components/ui/avatar'
12311
+ import {
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'
11939
12322
  import { Badge } from '@cms/components/ui/badge'
11940
12323
  import { Button } from '@cms/components/ui/button'
11941
12324
  import {
@@ -11948,7 +12331,10 @@ import {
11948
12331
  } from '@cms/components/ui/dropdown-menu'
11949
12332
  import type { UserData } from '@cms/types/auth'
11950
12333
  import type { ColumnDef } from '@tanstack/react-table'
11951
- 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'
11952
12338
  import { EditRoleDialog } from './edit-role-dialog'
11953
12339
 
11954
12340
  function getInitials(nameOrEmail: string): string {
@@ -11962,6 +12348,77 @@ function getInitials(nameOrEmail: string): string {
11962
12348
  .join('')
11963
12349
  }
11964
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
+
11965
12422
  export const columns: ColumnDef<UserData>[] = [
11966
12423
  {
11967
12424
  accessorKey: 'email',
@@ -12060,30 +12517,37 @@ export const columns: ColumnDef<UserData>[] = [
12060
12517
  },
12061
12518
  {
12062
12519
  id: 'actions',
12063
- cell: ({ row }) => (
12064
- <div className="flex justify-end">
12065
- <DropdownMenu>
12066
- <DropdownMenuTrigger asChild>
12067
- <Button variant="ghost" className="size-8 p-0">
12068
- <span className="sr-only">Open menu</span>
12069
- <MoreHorizontal className="size-4" />
12070
- </Button>
12071
- </DropdownMenuTrigger>
12072
- <DropdownMenuContent align="end">
12073
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
12074
- <DropdownMenuItem
12075
- onClick={() => navigator.clipboard.writeText(row.original.id)}
12076
- >
12077
- Copy user ID
12078
- </DropdownMenuItem>
12079
- <DropdownMenuSeparator />
12080
- <DropdownMenuItem className="text-destructive">
12081
- Delete user
12082
- </DropdownMenuItem>
12083
- </DropdownMenuContent>
12084
- </DropdownMenu>
12085
- </div>
12086
- )
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
+ }
12087
12551
  }
12088
12552
  ]
12089
12553
  `;
@@ -12102,7 +12566,7 @@ export default function UsersPage() {
12102
12566
  <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
12103
12567
  <PageHeader
12104
12568
  title="Users"
12105
- description="Manage and view all registered users"
12569
+ description="Manage all CMS users"
12106
12570
  >
12107
12571
  <CreateUserDialog />
12108
12572
  </PageHeader>
@@ -12262,30 +12726,30 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
12262
12726
  function scaffoldLayout({ cwd, config }) {
12263
12727
  const created = [];
12264
12728
  function write(relPath, content) {
12265
- const fullPath = path31.resolve(cwd, relPath);
12266
- ensureDir(path31.dirname(fullPath));
12729
+ const fullPath = path32.resolve(cwd, relPath);
12730
+ ensureDir(path32.dirname(fullPath));
12267
12731
  if (safeWriteFile(fullPath, content)) {
12268
12732
  created.push(relPath);
12269
12733
  }
12270
12734
  }
12271
- const cmsDir = path31.dirname(config.paths.pages);
12272
- write(path31.join(cmsDir, "layout.tsx"), cmsLayoutTemplate());
12273
- write(path31.join(config.paths.pages, "layout.tsx"), authenticatedLayoutTemplate());
12274
- write(path31.join(config.paths.login, "page.tsx"), loginPageTemplate());
12275
- write(path31.join(config.paths.login, "login-form.tsx"), loginFormTemplate());
12276
- write(path31.join(config.paths.pages, "page.tsx"), dashboardPageTemplate());
12277
- const usersDir = path31.join(config.paths.pages, "users");
12278
- write(path31.join(usersDir, "page.tsx"), usersPageTemplate());
12279
- write(path31.join(usersDir, "users-table.tsx"), usersTableTemplate());
12280
- write(path31.join(usersDir, "columns.tsx"), usersColumnsTemplate());
12281
- write(path31.join(usersDir, "create-user-dialog.tsx"), createUserDialogTemplate());
12282
- 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());
12283
12747
  return created;
12284
12748
  }
12285
12749
 
12286
12750
  // src/init/scaffolders/preset.ts
12287
- import fs27 from "fs";
12288
- import path32 from "path";
12751
+ import fs28 from "fs";
12752
+ import path33 from "path";
12289
12753
 
12290
12754
  // src/init/templates/presets/blog-categories.ts
12291
12755
  function blogCategoriesSchema() {
@@ -12295,6 +12759,7 @@ function blogCategoriesSchema() {
12295
12759
  label: "Categories",
12296
12760
  description: "Organize posts with categories",
12297
12761
  icon: "Tag",
12762
+ navGroup: { label: "Blog", icon: "BookOpen" },
12298
12763
  fields: [
12299
12764
  {
12300
12765
  name: "name",
@@ -12376,6 +12841,7 @@ function blogPostsSchema() {
12376
12841
  label: "Posts",
12377
12842
  description: "Manage blog posts and articles",
12378
12843
  icon: "FileText",
12844
+ navGroup: { label: "Blog", icon: "BookOpen" },
12379
12845
  fields: [
12380
12846
  {
12381
12847
  name: "title",
@@ -12471,17 +12937,11 @@ function defaultSettingsSchema() {
12471
12937
  description: "General site settings",
12472
12938
  icon: "Settings",
12473
12939
  fields: [
12474
- { name: "siteName", type: "string", label: "Site Name", required: true },
12940
+ { name: "siteName", type: "string", label: "Site Name", default: "BetterStart" },
12475
12941
  { name: "tagline", type: "string", label: "Tagline" },
12476
12942
  { name: "separator1", type: "separator" },
12477
12943
  { name: "logo", type: "image", label: "Logo" },
12478
- { name: "favicon", type: "image", label: "Favicon" },
12479
- { name: "separator2", type: "separator" },
12480
- { name: "contactEmail", type: "string", label: "Contact Email" },
12481
- { name: "socialTwitter", type: "string", label: "Twitter / X URL" },
12482
- { name: "socialInstagram", type: "string", label: "Instagram URL" },
12483
- { name: "socialLinkedin", type: "string", label: "LinkedIn URL" },
12484
- { name: "socialGithub", type: "string", label: "GitHub URL" }
12944
+ { name: "favicon", type: "image", label: "Favicon" }
12485
12945
  ]
12486
12946
  },
12487
12947
  null,
@@ -12693,15 +13153,15 @@ function scaffoldPreset({
12693
13153
  generatedFiles: [],
12694
13154
  errors: []
12695
13155
  };
12696
- const schemasDir = path32.join(cwd, config.paths?.schemas ?? "./cms/schemas");
13156
+ const schemasDir = path33.join(cwd, config.paths?.schemas ?? "./cms/schemas");
12697
13157
  const presetSchemas = getPresetSchemas(preset);
12698
13158
  for (const ps of presetSchemas) {
12699
- const filePath = path32.join(schemasDir, ps.filename);
12700
- const dir = path32.dirname(filePath);
12701
- if (!fs27.existsSync(dir)) {
12702
- 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 });
12703
13163
  }
12704
- fs27.writeFileSync(filePath, ps.content, "utf-8");
13164
+ fs28.writeFileSync(filePath, ps.content, "utf-8");
12705
13165
  result.schemas.push(ps.filename);
12706
13166
  }
12707
13167
  for (const ps of presetSchemas) {
@@ -12736,8 +13196,8 @@ function scaffoldPreset({
12736
13196
  }
12737
13197
 
12738
13198
  // src/init/scaffolders/tailwind.ts
12739
- import fs28 from "fs";
12740
- import path33 from "path";
13199
+ import fs29 from "fs";
13200
+ import path34 from "path";
12741
13201
  var SOURCE_LINES = ['@source "../cms/**/*.{ts,tsx}";', '@source "./(cms)/**/*.{ts,tsx}";'];
12742
13202
  var SOURCE_LINES_SRC = ['@source "../../cms/**/*.{ts,tsx}";', '@source "./(cms)/**/*.{ts,tsx}";'];
12743
13203
  var CMS_THEME_BLOCK = `
@@ -12792,8 +13252,8 @@ function findMainCss(cwd) {
12792
13252
  "globals.css"
12793
13253
  ];
12794
13254
  for (const candidate of candidates) {
12795
- const filePath = path33.join(cwd, candidate);
12796
- if (fs28.existsSync(filePath)) {
13255
+ const filePath = path34.join(cwd, candidate);
13256
+ if (fs29.existsSync(filePath)) {
12797
13257
  return filePath;
12798
13258
  }
12799
13259
  }
@@ -12804,7 +13264,7 @@ function scaffoldTailwind(cwd, hasSrcDir) {
12804
13264
  if (!cssFile) {
12805
13265
  return { file: null, appended: false };
12806
13266
  }
12807
- let content = fs28.readFileSync(cssFile, "utf-8");
13267
+ let content = fs29.readFileSync(cssFile, "utf-8");
12808
13268
  let changed = false;
12809
13269
  const sourceLines = hasSrcDir ? SOURCE_LINES_SRC : SOURCE_LINES;
12810
13270
  const missingLines = sourceLines.filter((sl) => !content.includes(sl));
@@ -12856,14 +13316,14 @@ ${CMS_THEME_BLOCK}
12856
13316
  }
12857
13317
  }
12858
13318
  if (changed) {
12859
- fs28.writeFileSync(cssFile, content, "utf-8");
13319
+ fs29.writeFileSync(cssFile, content, "utf-8");
12860
13320
  }
12861
13321
  return { file: cssFile, appended: changed };
12862
13322
  }
12863
13323
 
12864
13324
  // src/init/scaffolders/tsconfig.ts
12865
- import fs29 from "fs";
12866
- import path34 from "path";
13325
+ import fs30 from "fs";
13326
+ import path35 from "path";
12867
13327
  function stripJsonComments(input) {
12868
13328
  let result = "";
12869
13329
  let i = 0;
@@ -12913,14 +13373,14 @@ var CMS_PATH_ALIASES = {
12913
13373
  "@cms/cache/*": ["./cms/lib/cache/*"]
12914
13374
  };
12915
13375
  function scaffoldTsconfig(cwd) {
12916
- const tsconfigPath = path34.join(cwd, "tsconfig.json");
13376
+ const tsconfigPath = path35.join(cwd, "tsconfig.json");
12917
13377
  const added = [];
12918
13378
  const skipped = [];
12919
- if (!fs29.existsSync(tsconfigPath)) {
13379
+ if (!fs30.existsSync(tsconfigPath)) {
12920
13380
  skipped.push("tsconfig.json not found");
12921
13381
  return { added, skipped };
12922
13382
  }
12923
- const raw = fs29.readFileSync(tsconfigPath, "utf-8");
13383
+ const raw = fs30.readFileSync(tsconfigPath, "utf-8");
12924
13384
  const stripped = stripJsonComments(raw).replace(/,\s*([\]}])/g, "$1");
12925
13385
  let tsconfig;
12926
13386
  try {
@@ -12941,350 +13401,551 @@ function scaffoldTsconfig(cwd) {
12941
13401
  }
12942
13402
  compilerOptions.paths = paths;
12943
13403
  tsconfig.compilerOptions = compilerOptions;
12944
- fs29.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
13404
+ fs30.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
12945
13405
  `, "utf-8");
12946
13406
  return { added, skipped };
12947
13407
  }
12948
13408
 
12949
- // src/utils/detect.ts
12950
- import fs30 from "fs";
12951
- import path35 from "path";
12952
- var NEXT_CONFIG_FILES = ["next.config.ts", "next.config.js", "next.config.mjs"];
12953
- function detectProject(cwd) {
12954
- const isExisting = NEXT_CONFIG_FILES.some((f) => fs30.existsSync(path35.join(cwd, f)));
12955
- const hasSrcDir = fs30.existsSync(path35.join(cwd, "src"));
12956
- const hasTypeScript = fs30.existsSync(path35.join(cwd, "tsconfig.json")) || fs30.existsSync(path35.join(cwd, "tsconfig.app.json"));
12957
- const hasTailwind = detectTailwind(cwd);
12958
- const linter = detectLinter(cwd);
12959
- const conflicts = [];
12960
- if (isExisting) {
12961
- if (fs30.existsSync(path35.join(cwd, "cms"))) {
12962
- conflicts.push("cms/ directory already exists");
12963
- }
12964
- if (fs30.existsSync(path35.join(cwd, "cms.config.ts"))) {
12965
- conflicts.push("cms.config.ts already exists");
12966
- }
12967
- const appBase = hasSrcDir ? "src/app" : "app";
12968
- if (fs30.existsSync(path35.join(cwd, appBase, "(cms)"))) {
12969
- conflicts.push(`${appBase}/(cms)/ route group already exists`);
12970
- }
12971
- if (hasTsconfigCmsAliases(cwd)) {
12972
- conflicts.push("@cms/* path aliases already exist in tsconfig.json");
12973
- }
12974
- if (hasEnvBetterstartVars(cwd)) {
12975
- conflicts.push("BETTERSTART_* variables already exist in .env.local");
12976
- }
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)
12977
13472
  }
12978
- 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)
12979
13482
  }
12980
- var BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"];
12981
- var ESLINT_CONFIG_FILES = [
12982
- "eslint.config.js",
12983
- "eslint.config.mjs",
12984
- "eslint.config.cjs",
12985
- "eslint.config.ts",
12986
- ".eslintrc.json",
12987
- ".eslintrc.js",
12988
- ".eslintrc.cjs",
12989
- ".eslintrc.yml",
12990
- ".eslintrc.yaml",
12991
- ".eslintrc"
12992
- ];
12993
- function detectLinter(cwd) {
12994
- for (const f of BIOME_CONFIG_FILES) {
12995
- if (fs30.existsSync(path35.join(cwd, f))) {
12996
- return { type: "biome", configFile: f };
12997
- }
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);
12998
13499
  }
12999
- for (const f of ESLINT_CONFIG_FILES) {
13000
- if (fs30.existsSync(path35.join(cwd, f))) {
13001
- 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";
13002
13506
  }
13507
+ });
13508
+ if (clack.isCancel(email)) {
13509
+ clack.cancel("Cancelled.");
13510
+ process.exit(0);
13003
13511
  }
13004
- const pkgPath = path35.join(cwd, "package.json");
13005
- if (fs30.existsSync(pkgPath)) {
13006
- try {
13007
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
13008
- if (pkg.eslintConfig) {
13009
- return { type: "eslint", configFile: "package.json (eslintConfig)" };
13010
- }
13011
- } 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";
13012
13516
  }
13517
+ });
13518
+ if (clack.isCancel(password3)) {
13519
+ clack.cancel("Cancelled.");
13520
+ process.exit(0);
13013
13521
  }
13014
- return { type: "none", configFile: null };
13015
- }
13016
- function detectTailwind(cwd) {
13017
- const cssFiles = ["globals.css", "app.css", "index.css"].flatMap((f) => [
13018
- path35.join(cwd, "src", "app", f),
13019
- path35.join(cwd, "app", f),
13020
- path35.join(cwd, "src", f),
13021
- path35.join(cwd, f)
13022
- ]);
13023
- for (const cssFile of cssFiles) {
13024
- if (fs30.existsSync(cssFile)) {
13025
- const content = fs30.readFileSync(cssFile, "utf-8");
13026
- if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'") || content.includes("@theme")) {
13027
- return true;
13028
- }
13029
- }
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);
13030
13530
  }
13031
- const postcssFiles = ["postcss.config.js", "postcss.config.mjs", "postcss.config.cjs"];
13032
- for (const f of postcssFiles) {
13033
- if (fs30.existsSync(path35.join(cwd, f))) {
13034
- const content = fs30.readFileSync(path35.join(cwd, f), "utf-8");
13035
- if (content.includes("tailwindcss") || content.includes("@tailwindcss")) {
13036
- return true;
13037
- }
13038
- }
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 });
13039
13535
  }
13040
- return false;
13041
- }
13042
- function hasTsconfigCmsAliases(cwd) {
13043
- const tsconfigPath = path35.join(cwd, "tsconfig.json");
13044
- if (!fs30.existsSync(tsconfigPath)) return false;
13536
+ fs31.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13537
+ const spinner3 = clack.spinner();
13538
+ spinner3.start("Creating admin user...");
13045
13539
  try {
13046
- const content = fs30.readFileSync(tsconfigPath, "utf-8");
13047
- return content.includes("@cms/");
13048
- } catch {
13049
- 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);
13050
13562
  }
13051
- }
13052
- function hasEnvBetterstartVars(cwd) {
13053
- const envPath = path35.join(cwd, ".env.local");
13054
- if (!fs30.existsSync(envPath)) return false;
13055
13563
  try {
13056
- const content = fs30.readFileSync(envPath, "utf-8");
13057
- return content.includes("BETTERSTART_");
13564
+ fs31.unlinkSync(seedPath);
13565
+ if (fs31.existsSync(scriptsDir) && fs31.readdirSync(scriptsDir).length === 0) {
13566
+ fs31.rmdirSync(scriptsDir);
13567
+ }
13058
13568
  } catch {
13059
- return false;
13060
13569
  }
13061
- }
13570
+ clack.outro(`Admin user ready: ${email}`);
13571
+ });
13062
13572
 
13063
13573
  // src/commands/init.ts
13064
- 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) => {
13065
- p3.intro(pc.bgCyan(pc.black(" BetterStart CMS ")));
13066
- let cwd = process.cwd();
13067
- let project = detectProject(cwd);
13068
- let pm = detectPackageManager(cwd);
13069
- p3.log.info(`Package manager: ${pc.cyan(pm)}`);
13070
- let srcDir;
13071
- if (project.isExisting) {
13072
- p3.log.info(`Existing Next.js project detected`);
13073
- srcDir = project.hasSrcDir;
13074
- if (!project.hasTypeScript) {
13075
- p3.log.error("TypeScript is required. Please add a tsconfig.json first.");
13076
- process.exit(1);
13077
- }
13078
- if (project.conflicts.length > 0) {
13079
- p3.log.error("Conflicts detected:");
13080
- for (const conflict of project.conflicts) {
13081
- 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
+ p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
13584
+ let srcDir;
13585
+ if (project.isExisting) {
13586
+ p4.log.info(`Existing Next.js project detected`);
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);
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
+ }
13082
13607
  }
13083
- if (!options.yes) {
13084
- const proceed = await p3.confirm({
13085
- message: "Continue anyway? (existing files will NOT be overwritten)",
13086
- initialValue: true
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;
13612
+ const cnaSpinner = p4.spinner();
13613
+ cnaSpinner.start(`Creating Next.js app: ${projectPrompt.projectName}...`);
13614
+ try {
13615
+ const cnaArgs = [
13616
+ "create-next-app@latest",
13617
+ projectPrompt.projectName,
13618
+ "--typescript",
13619
+ "--tailwind",
13620
+ "--app",
13621
+ "--no-git",
13622
+ "--no-import-alias",
13623
+ "--turbopack"
13624
+ ];
13625
+ if (srcDir) cnaArgs.push("--src-dir");
13626
+ else cnaArgs.push("--no-src-dir");
13627
+ execFileSync4("npx", cnaArgs, {
13628
+ cwd,
13629
+ stdio: "pipe",
13630
+ timeout: 12e4
13087
13631
  });
13088
- if (p3.isCancel(proceed) || !proceed) {
13089
- p3.cancel("Setup cancelled.");
13090
- process.exit(0);
13632
+ cnaSpinner.stop(`Created ${projectPrompt.projectName}`);
13633
+ } catch (err) {
13634
+ cnaSpinner.stop("Failed to create Next.js app");
13635
+ p4.log.error(err instanceof Error ? err.message : "create-next-app failed");
13636
+ p4.log.info(
13637
+ `You can create the project manually:
13638
+ ${pc2.cyan(`npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`)}
13639
+ Then run ${pc2.cyan("betterstart init")} inside it.`
13640
+ );
13641
+ process.exit(1);
13642
+ }
13643
+ cwd = path37.resolve(cwd, projectPrompt.projectName);
13644
+ project = detectProject(cwd);
13645
+ pm = detectPackageManager(cwd);
13646
+ }
13647
+ const features = options.yes ? { includeEmail: true, preset: options.preset } : await promptFeatures(options.preset);
13648
+ let databaseUrl;
13649
+ const existingDbUrl = readExistingDbUrl(cwd);
13650
+ if (options.yes) {
13651
+ if (options.databaseUrl) {
13652
+ if (!isValidDbUrl(options.databaseUrl)) {
13653
+ p4.log.error(
13654
+ `Invalid database URL. Must start with ${pc2.cyan("postgres://")} or ${pc2.cyan("postgresql://")}`
13655
+ );
13656
+ process.exit(1);
13091
13657
  }
13658
+ databaseUrl = options.databaseUrl;
13659
+ } else if (existingDbUrl) {
13660
+ databaseUrl = existingDbUrl;
13661
+ }
13662
+ } else if (existingDbUrl) {
13663
+ const masked = maskDbUrl(existingDbUrl);
13664
+ p4.log.info(`Using existing database URL from .env.local ${pc2.dim(`(${masked})`)}`);
13665
+ databaseUrl = existingDbUrl;
13666
+ } else {
13667
+ const dbResult = await promptDatabase();
13668
+ databaseUrl = dbResult.url;
13669
+ }
13670
+ const config = {
13671
+ ...getDefaultConfig(srcDir),
13672
+ features: { email: features.includeEmail }
13673
+ };
13674
+ const s = p4.spinner();
13675
+ s.start("Creating CMS directory structure...");
13676
+ const baseFiles = scaffoldBase({ cwd, config });
13677
+ s.stop(`Created ${baseFiles.length} files`);
13678
+ s.start("Configuring TypeScript path aliases...");
13679
+ const tsResult = scaffoldTsconfig(cwd);
13680
+ s.stop(`Added ${tsResult.added.length} path aliases`);
13681
+ s.start("Configuring Tailwind CSS...");
13682
+ const twResult = scaffoldTailwind(cwd, srcDir);
13683
+ if (twResult.appended) {
13684
+ s.stop(`Updated ${twResult.file}`);
13685
+ } else if (twResult.file) {
13686
+ s.stop("Tailwind already configured for CMS");
13687
+ } else {
13688
+ s.stop("No CSS file found (will configure later)");
13689
+ }
13690
+ s.start("Setting up environment variables...");
13691
+ const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
13692
+ const envParts = [`Added ${envResult.added.length}`];
13693
+ if (envResult.updated.length > 0) envParts.push(`updated ${envResult.updated.length}`);
13694
+ s.stop(`${envParts.join(", ")} env vars in .env.local`);
13695
+ s.start("Setting up database...");
13696
+ const dbFiles = scaffoldDatabase({ cwd, config });
13697
+ s.stop(`Created ${dbFiles.length} database files`);
13698
+ s.start("Setting up authentication...");
13699
+ const authFiles = scaffoldAuth({ cwd, config });
13700
+ s.stop(`Created ${authFiles.length} auth files`);
13701
+ s.start("Copying CMS components...");
13702
+ const compFiles = scaffoldComponents({ cwd, config });
13703
+ s.stop(`Created ${compFiles.length} component files`);
13704
+ s.start("Creating CMS pages and layouts...");
13705
+ const layoutFiles = scaffoldLayout({ cwd, config });
13706
+ s.stop(`Created ${layoutFiles.length} page files`);
13707
+ s.start("Creating API routes...");
13708
+ const apiFiles = scaffoldApiRoutes({ cwd, config });
13709
+ s.stop(`Created ${apiFiles.length} API routes`);
13710
+ s.start("Checking for linter...");
13711
+ if (project.linter.type === "none") {
13712
+ s.stop("No linter found");
13713
+ s.start("Setting up Biome linter...");
13714
+ const biomeResult = scaffoldBiome(cwd, project.linter);
13715
+ if (biomeResult.installed) {
13716
+ s.stop("Created biome.json");
13717
+ } else {
13718
+ s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13092
13719
  }
13720
+ } else {
13721
+ s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
13093
13722
  }
13094
- } else {
13095
- p3.log.info("No Next.js project found \u2014 fresh project mode");
13096
- const projectPrompt = await promptProject(name);
13097
- srcDir = projectPrompt.useSrcDir;
13098
- const cnaSpinner = p3.spinner();
13099
- cnaSpinner.start(`Creating Next.js app: ${projectPrompt.projectName}...`);
13100
- try {
13101
- const cnaArgs = [
13102
- "create-next-app@latest",
13103
- projectPrompt.projectName,
13104
- "--typescript",
13105
- "--tailwind",
13106
- "--app",
13107
- "--no-git",
13108
- "--no-import-alias",
13109
- "--turbopack"
13110
- ];
13111
- if (srcDir) cnaArgs.push("--src-dir");
13112
- else cnaArgs.push("--no-src-dir");
13113
- execFileSync3("npx", cnaArgs, {
13114
- cwd,
13115
- stdio: "pipe",
13116
- timeout: 12e4
13117
- });
13118
- cnaSpinner.stop(`Created ${projectPrompt.projectName}`);
13119
- } catch (err) {
13120
- cnaSpinner.stop("Failed to create Next.js app");
13121
- p3.log.error(err instanceof Error ? err.message : "create-next-app failed");
13122
- p3.log.info(
13123
- `You can create the project manually:
13124
- ${pc.cyan(`npx create-next-app@latest ${projectPrompt.projectName} --typescript --tailwind --app`)}
13125
- Then run ${pc.cyan("betterstart init")} inside it.`
13723
+ s.start("Installing dependencies (this may take a minute)...");
13724
+ const depsResult = await installDependenciesAsync({
13725
+ cwd,
13726
+ pm,
13727
+ includeEmail: features.includeEmail,
13728
+ includeBiome: project.linter.type === "none"
13729
+ });
13730
+ if (depsResult.success) {
13731
+ s.stop(
13732
+ `Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`
13733
+ );
13734
+ } else {
13735
+ s.stop("Failed to install dependencies");
13736
+ p4.log.warning(depsResult.error ?? "Unknown error");
13737
+ p4.log.info(
13738
+ `You can install them manually:
13739
+ ${pc2.cyan(`${pm} add ${depsResult.coreDeps.join(" ")}`)}
13740
+ ${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13126
13741
  );
13127
- process.exit(1);
13128
13742
  }
13129
- cwd = path36.resolve(cwd, projectPrompt.projectName);
13130
- project = detectProject(cwd);
13131
- pm = detectPackageManager(cwd);
13132
- }
13133
- const features = options.yes ? { includeEmail: true, preset: options.preset } : await promptFeatures(options.preset);
13134
- const config = {
13135
- ...getDefaultConfig(srcDir),
13136
- features: { email: features.includeEmail }
13137
- };
13138
- const s = p3.spinner();
13139
- s.start("Creating CMS directory structure...");
13140
- const baseFiles = scaffoldBase({ cwd, config });
13141
- s.stop(`Created ${baseFiles.length} files`);
13142
- s.start("Configuring TypeScript path aliases...");
13143
- const tsResult = scaffoldTsconfig(cwd);
13144
- s.stop(`Added ${tsResult.added.length} path aliases`);
13145
- s.start("Configuring Tailwind CSS...");
13146
- const twResult = scaffoldTailwind(cwd, srcDir);
13147
- if (twResult.appended) {
13148
- s.stop(`Updated ${twResult.file}`);
13149
- } else if (twResult.file) {
13150
- s.stop("Tailwind already configured for CMS");
13151
- } else {
13152
- s.stop("No CSS file found (will configure later)");
13153
- }
13154
- s.start("Setting up environment variables...");
13155
- const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail });
13156
- s.stop(`Added ${envResult.added.length} env vars to .env.local`);
13157
- s.start("Setting up database...");
13158
- const dbFiles = scaffoldDatabase({ cwd, config });
13159
- s.stop(`Created ${dbFiles.length} database files`);
13160
- s.start("Setting up authentication...");
13161
- const authFiles = scaffoldAuth({ cwd, config });
13162
- s.stop(`Created ${authFiles.length} auth files`);
13163
- s.start("Copying CMS components...");
13164
- const compFiles = scaffoldComponents({ cwd, config });
13165
- s.stop(`Created ${compFiles.length} component files`);
13166
- s.start("Creating CMS pages and layouts...");
13167
- const layoutFiles = scaffoldLayout({ cwd, config });
13168
- s.stop(`Created ${layoutFiles.length} page files`);
13169
- s.start("Creating API routes...");
13170
- const apiFiles = scaffoldApiRoutes({ cwd, config });
13171
- s.stop(`Created ${apiFiles.length} API routes`);
13172
- s.start("Checking for linter...");
13173
- if (project.linter.type === "none") {
13174
- s.stop("No linter found");
13175
- s.start("Setting up Biome linter...");
13176
- const biomeResult = scaffoldBiome(cwd, project.linter);
13177
- if (biomeResult.installed) {
13178
- s.stop("Created biome.json");
13743
+ s.start(`Applying ${features.preset} preset...`);
13744
+ const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13745
+ if (presetResult.errors.length > 0) {
13746
+ s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13747
+ for (const err of presetResult.errors) {
13748
+ p4.log.warning(` ${err}`);
13749
+ }
13179
13750
  } else {
13180
- s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13751
+ s.stop(
13752
+ `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13753
+ );
13181
13754
  }
13182
- } else {
13183
- s.stop(`Linter: ${pc.cyan(project.linter.type)} (${project.linter.configFile})`);
13184
- }
13185
- s.start("Installing dependencies (this may take a minute)...");
13186
- const depsResult = await installDependenciesAsync({
13187
- cwd,
13188
- pm,
13189
- includeEmail: features.includeEmail,
13190
- includeBiome: project.linter.type === "none"
13191
- });
13192
- if (depsResult.success) {
13193
- s.stop(`Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`);
13194
- } else {
13195
- s.stop("Failed to install dependencies");
13196
- p3.log.warning(depsResult.error ?? "Unknown error");
13197
- p3.log.info(
13198
- `You can install them manually:
13199
- ${pc.cyan(`${pm} add ${depsResult.coreDeps.join(" ")}`)}
13200
- ${pc.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13201
- );
13202
- }
13203
- s.start(`Applying ${features.preset} preset...`);
13204
- const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13205
- if (presetResult.errors.length > 0) {
13206
- s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13207
- for (const err of presetResult.errors) {
13208
- p3.log.warning(` ${err}`);
13755
+ let dbPushed = false;
13756
+ if (depsResult.success && hasDbUrl(cwd)) {
13757
+ s.start("Pushing database schema (drizzle-kit push)...");
13758
+ const pushResult = await runDrizzlePush(cwd);
13759
+ if (pushResult.success) {
13760
+ s.stop("Database schema pushed");
13761
+ dbPushed = true;
13762
+ } else {
13763
+ s.stop("Database push failed");
13764
+ p4.log.warning(pushResult.error ?? "Unknown error");
13765
+ p4.log.info(`You can run it manually: ${pc2.cyan("npx drizzle-kit push")}`);
13766
+ }
13767
+ }
13768
+ let seedEmail;
13769
+ let seedPassword;
13770
+ let seedSuccess = false;
13771
+ if (dbPushed && !options.yes) {
13772
+ p4.log.step("Create your admin account");
13773
+ const email = await p4.text({
13774
+ message: "Admin email",
13775
+ placeholder: "admin@example.com",
13776
+ validate: (v) => {
13777
+ if (!v || !v.includes("@")) return "Please enter a valid email";
13778
+ }
13779
+ });
13780
+ if (p4.isCancel(email)) {
13781
+ p4.cancel("Setup cancelled.");
13782
+ process.exit(0);
13783
+ }
13784
+ const password3 = await p4.password({
13785
+ message: "Admin password",
13786
+ validate: (v) => {
13787
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
13788
+ }
13789
+ });
13790
+ if (p4.isCancel(password3)) {
13791
+ p4.cancel("Setup cancelled.");
13792
+ process.exit(0);
13793
+ }
13794
+ seedEmail = email;
13795
+ seedPassword = password3;
13796
+ s.start("Creating admin user...");
13797
+ const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
13798
+ if (seedResult.success) {
13799
+ s.stop("Admin user created");
13800
+ seedSuccess = true;
13801
+ } else {
13802
+ s.stop("Failed to create admin user");
13803
+ p4.log.warning(seedResult.error ?? "Unknown error");
13804
+ p4.log.info(`You can run it manually: ${pc2.cyan("npx betterstart seed")}`);
13805
+ }
13209
13806
  }
13210
- } else {
13211
- s.stop(
13212
- `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13213
- );
13214
- }
13215
- if (depsResult.success && hasDbUrl(cwd)) {
13216
- s.start("Pushing database schema (drizzle-kit push)...");
13217
- const pushResult = await runDrizzlePush(cwd);
13218
- if (pushResult.success) {
13219
- s.stop("Database schema pushed");
13220
- } else {
13221
- s.stop("Database push failed");
13222
- p3.log.warning(pushResult.error ?? "Unknown error");
13223
- p3.log.info(`You can run it manually: ${pc.cyan("npx drizzle-kit push")}`);
13807
+ {
13808
+ const entityNames = [];
13809
+ const formNames = [];
13810
+ const schemasDir = path37.join(cwd, config.paths.schemas);
13811
+ const formsDir = path37.join(schemasDir, "forms");
13812
+ if (fs32.existsSync(schemasDir)) {
13813
+ for (const f of fs32.readdirSync(schemasDir)) {
13814
+ if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13815
+ }
13816
+ }
13817
+ if (fs32.existsSync(formsDir)) {
13818
+ for (const f of fs32.readdirSync(formsDir)) {
13819
+ if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13820
+ }
13821
+ }
13822
+ regenerateCmsDoc(cwd, config, {
13823
+ preset: features.preset,
13824
+ schemas: entityNames,
13825
+ forms: formNames
13826
+ });
13224
13827
  }
13225
- }
13226
- {
13227
- const entityNames = [];
13228
- const formNames = [];
13229
- const schemasDir = path36.join(cwd, config.paths.schemas);
13230
- const formsDir = path36.join(schemasDir, "forms");
13231
- if (fs31.existsSync(schemasDir)) {
13232
- for (const f of fs31.readdirSync(schemasDir)) {
13233
- if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13828
+ const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
13829
+ const summaryLines = [
13830
+ `Preset: ${pc2.cyan(features.preset)}`,
13831
+ `Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
13832
+ `Files created: ${pc2.cyan(String(totalFiles))}`,
13833
+ `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
13834
+ ];
13835
+ if (seedSuccess && seedEmail && seedPassword) {
13836
+ summaryLines.push(
13837
+ "",
13838
+ `Admin: ${pc2.cyan(seedEmail)}`,
13839
+ `Password: ${pc2.cyan(seedPassword)}`,
13840
+ `CMS: ${pc2.cyan("http://localhost:3000/cms/login")}`
13841
+ );
13842
+ }
13843
+ const nextSteps = [];
13844
+ let step = 1;
13845
+ const envStepLabel = databaseUrl ? `Fill in remaining values in ${pc2.cyan(".env.local")}` : `Fill in values in ${pc2.cyan(".env.local")}`;
13846
+ nextSteps.push(` ${step++}. ${envStepLabel}`);
13847
+ if (!dbPushed) {
13848
+ nextSteps.push(` ${step++}. Run ${pc2.cyan("npx drizzle-kit push")} to sync the database`);
13849
+ }
13850
+ if (!seedSuccess) {
13851
+ nextSteps.push(
13852
+ ` ${step++}. Run ${pc2.cyan("npx betterstart seed")} to create an admin user`
13853
+ );
13854
+ }
13855
+ nextSteps.push(` ${step++}. Run ${pc2.cyan("pnpm run dev")} to start the development server`);
13856
+ nextSteps.push(
13857
+ ` ${step++}. Run ${pc2.cyan("npx betterstart generate <schema>")} to create content types`
13858
+ );
13859
+ summaryLines.push("", "Next steps:", ...nextSteps);
13860
+ p4.note(summaryLines.join("\n"), "CMS scaffolded successfully");
13861
+ if (!options.yes) {
13862
+ const devCmd = runCommand(pm, "dev");
13863
+ const startDev = await p4.confirm({
13864
+ message: "Start the development server?",
13865
+ initialValue: true
13866
+ });
13867
+ if (!p4.isCancel(startDev) && startDev) {
13868
+ p4.outro(`Starting ${pc2.cyan(devCmd)}...`);
13869
+ const [bin, ...args] = devCmd.split(" ");
13870
+ spawn2(bin, args, { cwd, stdio: "inherit" });
13871
+ return;
13234
13872
  }
13235
13873
  }
13236
- if (fs31.existsSync(formsDir)) {
13237
- for (const f of fs31.readdirSync(formsDir)) {
13238
- if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13874
+ p4.outro("Done!");
13875
+ }
13876
+ );
13877
+ function isValidDbUrl(url) {
13878
+ return url.startsWith("postgres://") || url.startsWith("postgresql://");
13879
+ }
13880
+ function readExistingDbUrl(cwd) {
13881
+ const envPath = path37.join(cwd, ".env.local");
13882
+ if (!fs32.existsSync(envPath)) return void 0;
13883
+ const content = fs32.readFileSync(envPath, "utf-8");
13884
+ for (const line of content.split("\n")) {
13885
+ const trimmed = line.trim();
13886
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
13887
+ const [key, ...rest] = trimmed.split("=");
13888
+ if (key?.trim() === "BETTERSTART_DATABASE_URL") {
13889
+ const val = rest.join("=").replace(/^['"]|['"]$/g, "").trim();
13890
+ if (val.length > 0 && !val.startsWith("your_") && val !== "postgresql://..." && isValidDbUrl(val)) {
13891
+ return val;
13239
13892
  }
13240
13893
  }
13241
- regenerateCmsDoc(cwd, config, {
13242
- preset: features.preset,
13243
- schemas: entityNames,
13244
- forms: formNames
13245
- });
13246
13894
  }
13247
- const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
13248
- const dbPushed = depsResult.success && hasDbUrl(cwd);
13249
- const nextSteps = [];
13250
- let step = 1;
13251
- nextSteps.push(` ${step++}. Fill in values in ${pc.cyan(".env.local")}`);
13252
- if (!dbPushed) {
13253
- nextSteps.push(` ${step++}. Run ${pc.cyan("npx drizzle-kit push")} to sync the database`);
13254
- }
13255
- nextSteps.push(` ${step++}. Run ${pc.cyan("npx betterstart seed")} to create an admin user`);
13256
- nextSteps.push(
13257
- ` ${step++}. Run ${pc.cyan("npx betterstart generate <schema>")} to create content types`
13258
- );
13259
- p3.note(
13260
- [
13261
- `Preset: ${pc.cyan(features.preset)}`,
13262
- `Email: ${features.includeEmail ? pc.green("yes") : pc.dim("no")}`,
13263
- `Files created: ${pc.cyan(String(totalFiles))}`,
13264
- `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`,
13265
- "",
13266
- "Next steps:",
13267
- ...nextSteps
13268
- ].join("\n"),
13269
- "CMS scaffolded successfully"
13270
- );
13271
- p3.outro("Done!");
13272
- });
13895
+ return void 0;
13896
+ }
13897
+ function maskDbUrl(url) {
13898
+ try {
13899
+ const parsed = new URL(url);
13900
+ return `${parsed.protocol}//${parsed.host}/***`;
13901
+ } catch {
13902
+ return "postgres://***";
13903
+ }
13904
+ }
13273
13905
  function hasDbUrl(cwd) {
13274
- const envPath = path36.join(cwd, ".env.local");
13275
- if (!fs31.existsSync(envPath)) return false;
13276
- const content = fs31.readFileSync(envPath, "utf-8");
13906
+ const envPath = path37.join(cwd, ".env.local");
13907
+ if (!fs32.existsSync(envPath)) return false;
13908
+ const content = fs32.readFileSync(envPath, "utf-8");
13277
13909
  for (const line of content.split("\n")) {
13278
13910
  const trimmed = line.trim();
13279
13911
  if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
13280
13912
  const [key, ...rest] = trimmed.split("=");
13281
13913
  if (key?.trim() === "BETTERSTART_DATABASE_URL") {
13282
13914
  const val = rest.join("=").trim();
13283
- return val.length > 0 && !val.startsWith("your_") && !val.startsWith('"your_');
13915
+ const unquoted = val.replace(/^['"]|['"]$/g, "");
13916
+ return unquoted.length > 0 && !unquoted.startsWith("your_") && unquoted !== "postgresql://...";
13284
13917
  }
13285
13918
  }
13286
13919
  return false;
13287
13920
  }
13921
+ async function runSeed(cwd, cmsDir, email, password3) {
13922
+ const scriptsDir = path37.join(cwd, cmsDir, "scripts");
13923
+ const seedPath = path37.join(scriptsDir, "seed.ts");
13924
+ if (!fs32.existsSync(scriptsDir)) {
13925
+ fs32.mkdirSync(scriptsDir, { recursive: true });
13926
+ }
13927
+ fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13928
+ try {
13929
+ execFileSync4("npx", ["tsx", seedPath], {
13930
+ cwd,
13931
+ stdio: "pipe",
13932
+ timeout: 3e4,
13933
+ env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
13934
+ });
13935
+ return { success: true, error: null };
13936
+ } catch (err) {
13937
+ const msg = err instanceof Error ? err.message : String(err);
13938
+ return { success: false, error: msg };
13939
+ } finally {
13940
+ try {
13941
+ fs32.unlinkSync(seedPath);
13942
+ if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
13943
+ fs32.rmdirSync(scriptsDir);
13944
+ }
13945
+ } catch {
13946
+ }
13947
+ }
13948
+ }
13288
13949
  function runDrizzlePush(cwd) {
13289
13950
  return new Promise((resolve) => {
13290
13951
  const child = spawn2("npx", ["drizzle-kit", "push", "--force"], {
@@ -13307,16 +13968,16 @@ function runDrizzlePush(cwd) {
13307
13968
  }
13308
13969
 
13309
13970
  // src/commands/remove.ts
13310
- import fs32 from "fs";
13311
- import path37 from "path";
13971
+ import fs33 from "fs";
13972
+ import path38 from "path";
13312
13973
  import readline from "readline";
13313
- import { Command as Command3 } from "commander";
13974
+ import { Command as Command4 } from "commander";
13314
13975
  function toPascalCase17(str) {
13315
13976
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
13316
13977
  }
13317
13978
  function toCamelCase8(str) {
13318
- const p4 = toPascalCase17(str);
13319
- return p4.charAt(0).toLowerCase() + p4.slice(1);
13979
+ const p5 = toPascalCase17(str);
13980
+ return p5.charAt(0).toLowerCase() + p5.slice(1);
13320
13981
  }
13321
13982
  function singularize13(str) {
13322
13983
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -13358,8 +14019,8 @@ function findTableEnd2(content, startIndex) {
13358
14019
  return content.length;
13359
14020
  }
13360
14021
  function removeTableFromSchema(schemaFilePath, name) {
13361
- if (!fs32.existsSync(schemaFilePath)) return false;
13362
- let content = fs32.readFileSync(schemaFilePath, "utf-8");
14022
+ if (!fs33.existsSync(schemaFilePath)) return false;
14023
+ let content = fs33.readFileSync(schemaFilePath, "utf-8");
13363
14024
  const variableName = toCamelCase8(name);
13364
14025
  let changed = false;
13365
14026
  if (content.includes(`export const ${variableName} =`)) {
@@ -13387,13 +14048,13 @@ function removeTableFromSchema(schemaFilePath, name) {
13387
14048
  }
13388
14049
  if (changed) {
13389
14050
  content = content.replace(/\n{3,}/g, "\n\n");
13390
- fs32.writeFileSync(schemaFilePath, content, "utf-8");
14051
+ fs33.writeFileSync(schemaFilePath, content, "utf-8");
13391
14052
  }
13392
14053
  return changed;
13393
14054
  }
13394
14055
  function removeFromNavigation(navFilePath, name) {
13395
- if (!fs32.existsSync(navFilePath)) return false;
13396
- const content = fs32.readFileSync(navFilePath, "utf-8");
14056
+ if (!fs33.existsSync(navFilePath)) return false;
14057
+ const content = fs33.readFileSync(navFilePath, "utf-8");
13397
14058
  const href = `/cms/${name}`;
13398
14059
  if (!content.includes(`'${href}'`)) return false;
13399
14060
  const lines = content.split("\n");
@@ -13424,7 +14085,7 @@ function removeFromNavigation(navFilePath, name) {
13424
14085
  if (startLine === -1 || endLine === -1) return false;
13425
14086
  lines.splice(startLine, endLine - startLine + 1);
13426
14087
  const updated = lines.join("\n").replace(/,\s*,/g, ",").replace(/\[\s*,/, "[");
13427
- fs32.writeFileSync(navFilePath, updated, "utf-8");
14088
+ fs33.writeFileSync(navFilePath, updated, "utf-8");
13428
14089
  return true;
13429
14090
  }
13430
14091
  async function promptConfirm(message) {
@@ -13439,8 +14100,8 @@ async function promptConfirm(message) {
13439
14100
  });
13440
14101
  });
13441
14102
  }
13442
- 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) => {
13443
- const cwd = options.cwd ? path37.resolve(options.cwd) : process.cwd();
14103
+ 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) => {
14104
+ const cwd = options.cwd ? path38.resolve(options.cwd) : process.cwd();
13444
14105
  console.log("\n BetterStart Remove\n");
13445
14106
  let config;
13446
14107
  try {
@@ -13453,34 +14114,34 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13453
14114
  const pagesDir = config.paths?.pages ?? "./src/app/(cms)/cms/(authenticated)";
13454
14115
  const kebabName = toKebabCase9(schemaName);
13455
14116
  const targets = [];
13456
- const entityPagesDir = path37.join(cwd, pagesDir, schemaName);
13457
- if (fs32.existsSync(entityPagesDir)) {
14117
+ const entityPagesDir = path38.join(cwd, pagesDir, schemaName);
14118
+ if (fs33.existsSync(entityPagesDir)) {
13458
14119
  targets.push({
13459
14120
  path: entityPagesDir,
13460
- label: `${path37.join(pagesDir, schemaName)}/`,
14121
+ label: `${path38.join(pagesDir, schemaName)}/`,
13461
14122
  isDir: true
13462
14123
  });
13463
14124
  }
13464
- const actionsFile = path37.join(cwd, cmsDir, "lib", "actions", `${kebabName}.ts`);
13465
- if (fs32.existsSync(actionsFile)) {
14125
+ const actionsFile = path38.join(cwd, cmsDir, "lib", "actions", `${kebabName}.ts`);
14126
+ if (fs33.existsSync(actionsFile)) {
13466
14127
  targets.push({
13467
14128
  path: actionsFile,
13468
- label: path37.join(cmsDir, "lib", "actions", `${kebabName}.ts`),
14129
+ label: path38.join(cmsDir, "lib", "actions", `${kebabName}.ts`),
13469
14130
  isDir: false
13470
14131
  });
13471
14132
  }
13472
- const hookFile = path37.join(cwd, cmsDir, "hooks", `use-${kebabName}.ts`);
13473
- if (fs32.existsSync(hookFile)) {
14133
+ const hookFile = path38.join(cwd, cmsDir, "hooks", `use-${kebabName}.ts`);
14134
+ if (fs33.existsSync(hookFile)) {
13474
14135
  targets.push({
13475
14136
  path: hookFile,
13476
- label: path37.join(cmsDir, "hooks", `use-${kebabName}.ts`),
14137
+ label: path38.join(cmsDir, "hooks", `use-${kebabName}.ts`),
13477
14138
  isDir: false
13478
14139
  });
13479
14140
  }
13480
- const schemaFilePath = path37.join(cwd, cmsDir, "db", "schema.ts");
13481
- const hasTable = fs32.existsSync(schemaFilePath) && fs32.readFileSync(schemaFilePath, "utf-8").includes(`export const ${toCamelCase8(schemaName)} =`);
13482
- const navFilePath = path37.join(cwd, cmsDir, "data", "navigation.ts");
13483
- const hasNavEntry = fs32.existsSync(navFilePath) && fs32.readFileSync(navFilePath, "utf-8").includes(`'/cms/${schemaName}'`);
14141
+ const schemaFilePath = path38.join(cwd, cmsDir, "db", "schema.ts");
14142
+ const hasTable = fs33.existsSync(schemaFilePath) && fs33.readFileSync(schemaFilePath, "utf-8").includes(`export const ${toCamelCase8(schemaName)} =`);
14143
+ const navFilePath = path38.join(cwd, cmsDir, "data", "navigation.ts");
14144
+ const hasNavEntry = fs33.existsSync(navFilePath) && fs33.readFileSync(navFilePath, "utf-8").includes(`'/cms/${schemaName}'`);
13484
14145
  if (targets.length === 0 && !hasTable && !hasNavEntry) {
13485
14146
  console.log(` No generated files found for: ${schemaName}`);
13486
14147
  return;
@@ -13490,10 +14151,10 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13490
14151
  console.log(` ${t.isDir ? "[dir]" : " "} ${t.label}`);
13491
14152
  }
13492
14153
  if (hasTable) {
13493
- console.log(` [edit] ${path37.join(cmsDir, "db", "schema.ts")} (remove table)`);
14154
+ console.log(` [edit] ${path38.join(cmsDir, "db", "schema.ts")} (remove table)`);
13494
14155
  }
13495
14156
  if (hasNavEntry) {
13496
- console.log(` [edit] ${path37.join(cmsDir, "data", "navigation.ts")} (remove entry)`);
14157
+ console.log(` [edit] ${path38.join(cmsDir, "data", "navigation.ts")} (remove entry)`);
13497
14158
  }
13498
14159
  if (!options.force) {
13499
14160
  console.log("");
@@ -13506,19 +14167,19 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13506
14167
  console.log("");
13507
14168
  for (const t of targets) {
13508
14169
  if (t.isDir) {
13509
- fs32.rmSync(t.path, { recursive: true, force: true });
14170
+ fs33.rmSync(t.path, { recursive: true, force: true });
13510
14171
  } else {
13511
- fs32.unlinkSync(t.path);
14172
+ fs33.unlinkSync(t.path);
13512
14173
  }
13513
14174
  console.log(` Removed: ${t.label}`);
13514
14175
  }
13515
14176
  if (hasTable) {
13516
14177
  removeTableFromSchema(schemaFilePath, schemaName);
13517
- console.log(` Cleaned: ${path37.join(cmsDir, "db", "schema.ts")}`);
14178
+ console.log(` Cleaned: ${path38.join(cmsDir, "db", "schema.ts")}`);
13518
14179
  }
13519
14180
  if (hasNavEntry) {
13520
14181
  removeFromNavigation(navFilePath, schemaName);
13521
- console.log(` Cleaned: ${path37.join(cmsDir, "data", "navigation.ts")}`);
14182
+ console.log(` Cleaned: ${path38.join(cmsDir, "data", "navigation.ts")}`);
13522
14183
  }
13523
14184
  console.log("\n Removal complete!");
13524
14185
  console.log("\n Note: You may need to manually:");
@@ -13528,170 +14189,6 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove all g
13528
14189
  console.log("");
13529
14190
  });
13530
14191
 
13531
- // src/commands/seed.ts
13532
- import fs33 from "fs";
13533
- import path38 from "path";
13534
- import * as clack from "@clack/prompts";
13535
- import { Command as Command4 } from "commander";
13536
- function buildSeedScript() {
13537
- return `/**
13538
- * BetterStart CMS \u2014 Seed Script
13539
- * Creates the initial admin user
13540
- * AUTO-GENERATED \u2014 safe to delete after running
13541
- */
13542
-
13543
- import { loadEnvConfig } from '@next/env'
13544
- loadEnvConfig(process.cwd())
13545
-
13546
- import { neon } from '@neondatabase/serverless'
13547
- import { drizzle } from 'drizzle-orm/neon-http'
13548
- import { eq } from 'drizzle-orm'
13549
- import * as schema from '../db/schema'
13550
- import { betterAuth } from 'better-auth'
13551
- import { drizzleAdapter } from 'better-auth/adapters/drizzle'
13552
-
13553
- // Inline DB connection (mirrors cms/db/client.ts)
13554
- const sql = neon(process.env.BETTERSTART_DATABASE_URL!)
13555
- const db = drizzle({ client: sql, schema })
13556
-
13557
- // Inline auth setup (mirrors cms/lib/auth/auth.ts)
13558
- const auth = betterAuth({
13559
- secret: process.env.BETTERSTART_AUTH_SECRET,
13560
- baseURL: process.env.BETTERSTART_AUTH_URL,
13561
- basePath: process.env.BETTERSTART_AUTH_BASE_PATH || '/api/cms/auth',
13562
- database: drizzleAdapter(db, {
13563
- provider: 'pg',
13564
- schema: {
13565
- user: schema.user,
13566
- session: schema.session,
13567
- account: schema.account,
13568
- verification: schema.verification,
13569
- },
13570
- }),
13571
- emailAndPassword: { enabled: true, minPasswordLength: 8 },
13572
- user: {
13573
- additionalFields: {
13574
- role: { type: 'string', required: false, defaultValue: 'member', input: false },
13575
- },
13576
- },
13577
- })
13578
-
13579
- const EMAIL = process.env.SEED_EMAIL!
13580
- const PASSWORD = process.env.SEED_PASSWORD!
13581
- const NAME = process.env.SEED_NAME || 'Admin'
13582
-
13583
- async function main() {
13584
- console.log('\\n Creating admin user...')
13585
- console.log(\` Email: \${EMAIL}\\n\`)
13586
-
13587
- const result = await auth.api.signUpEmail({
13588
- body: { email: EMAIL, password: PASSWORD, name: NAME },
13589
- })
13590
-
13591
- if (!result?.user) {
13592
- console.error(' Failed to create user.')
13593
- process.exit(1)
13594
- }
13595
-
13596
- await db
13597
- .update(schema.user)
13598
- .set({ role: 'admin' })
13599
- .where(eq(schema.user.id, result.user.id))
13600
-
13601
- console.log(\` Admin user created: \${EMAIL}\`)
13602
- console.log(' Role: admin\\n')
13603
- process.exit(0)
13604
- }
13605
-
13606
- main().catch((err) => {
13607
- console.error(' Seed failed:', err.message || err)
13608
- process.exit(1)
13609
- })
13610
- `;
13611
- }
13612
- var seedCommand = new Command4("seed").description("Create the initial admin user").option("--cwd <path>", "Project root path").action(async (options) => {
13613
- const cwd = options.cwd ? path38.resolve(options.cwd) : process.cwd();
13614
- clack.intro("BetterStart Seed");
13615
- let config;
13616
- try {
13617
- config = await resolveConfig(cwd);
13618
- } catch (err) {
13619
- clack.cancel(`Error loading config: ${err instanceof Error ? err.message : String(err)}`);
13620
- process.exit(1);
13621
- }
13622
- const cmsDir = config.paths?.cms ?? "./cms";
13623
- const email = await clack.text({
13624
- message: "Admin email",
13625
- placeholder: "admin@example.com",
13626
- validate: (v) => {
13627
- if (!v || !v.includes("@")) return "Please enter a valid email";
13628
- }
13629
- });
13630
- if (clack.isCancel(email)) {
13631
- clack.cancel("Cancelled.");
13632
- process.exit(0);
13633
- }
13634
- const password2 = await clack.password({
13635
- message: "Admin password",
13636
- validate: (v) => {
13637
- if (!v || v.length < 8) return "Password must be at least 8 characters";
13638
- }
13639
- });
13640
- if (clack.isCancel(password2)) {
13641
- clack.cancel("Cancelled.");
13642
- process.exit(0);
13643
- }
13644
- const name = await clack.text({
13645
- message: "Admin name",
13646
- placeholder: "Admin",
13647
- defaultValue: "Admin"
13648
- });
13649
- if (clack.isCancel(name)) {
13650
- clack.cancel("Cancelled.");
13651
- process.exit(0);
13652
- }
13653
- const scriptsDir = path38.join(cwd, cmsDir, "scripts");
13654
- const seedPath = path38.join(scriptsDir, "seed.ts");
13655
- if (!fs33.existsSync(scriptsDir)) {
13656
- fs33.mkdirSync(scriptsDir, { recursive: true });
13657
- }
13658
- fs33.writeFileSync(seedPath, buildSeedScript(), "utf-8");
13659
- const spinner3 = clack.spinner();
13660
- spinner3.start("Creating admin user...");
13661
- try {
13662
- const { execFileSync: execFileSync4 } = await import("child_process");
13663
- execFileSync4("npx", ["tsx", seedPath], {
13664
- cwd,
13665
- stdio: "pipe",
13666
- env: {
13667
- ...process.env,
13668
- SEED_EMAIL: email,
13669
- SEED_PASSWORD: password2,
13670
- SEED_NAME: name || "Admin"
13671
- }
13672
- });
13673
- spinner3.stop("Admin user created");
13674
- } catch (err) {
13675
- spinner3.stop("Failed to create admin user");
13676
- const errMsg = err instanceof Error ? err.message : String(err);
13677
- clack.log.error(errMsg);
13678
- clack.log.info("You can run the seed script manually:");
13679
- clack.log.info(
13680
- ` SEED_EMAIL="${email}" SEED_PASSWORD="..." npx tsx ${path38.relative(cwd, seedPath)}`
13681
- );
13682
- clack.outro("");
13683
- process.exit(1);
13684
- }
13685
- try {
13686
- fs33.unlinkSync(seedPath);
13687
- if (fs33.existsSync(scriptsDir) && fs33.readdirSync(scriptsDir).length === 0) {
13688
- fs33.rmdirSync(scriptsDir);
13689
- }
13690
- } catch {
13691
- }
13692
- clack.outro(`Admin user ready: ${email}`);
13693
- });
13694
-
13695
14192
  // src/cli.ts
13696
14193
  var program = new Command5();
13697
14194
  program.name("betterstart").description("Scaffold a full-featured CMS into any Next.js 16 application").version("0.1.0");