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