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