@betterstart/cli 0.1.42 → 0.1.43
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/README.md +1 -258
- package/dist/cli.js +40 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,260 +1,3 @@
|
|
|
1
1
|
# @betterstart/cli
|
|
2
2
|
|
|
3
|
-
> **Warning:** This project is a work in progress. Expect bugs, breaking changes, and incomplete features.
|
|
4
|
-
|
|
5
|
-
Drop a production-ready CMS into any Next.js 16 app with one command. You get a full admin panel — auth, CRUD, rich text editing, file uploads, forms — living in a self-contained `cms/` directory. You own the code. No vendor lock-in.
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx @betterstart/cli init
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## What you get
|
|
12
|
-
|
|
13
|
-
- **Admin panel** with sidebar navigation, search, filters, bulk actions, and CSV export
|
|
14
|
-
- **Authentication** via Better Auth (email/password, role-based: admin, editor, member)
|
|
15
|
-
- **22 field types** including rich text (TipTap), markdown (CodeMirror + Shiki), image/video uploads, relationships, tabs, groups, lists, and curriculum builder
|
|
16
|
-
- **Public forms** with multi-step support, email notifications, Mailchimp integration, and a submissions admin
|
|
17
|
-
- **Three schema types** — entities (full CRUD), singles (settings pages), and forms (public-facing with submission tracking)
|
|
18
|
-
- **Caching** with Next.js 16 `'use cache'` directive, `cacheLife`, `cacheTag`, and `updateTag`
|
|
19
|
-
|
|
20
|
-
## Commands
|
|
21
|
-
|
|
22
|
-
### `betterstart init [name]`
|
|
23
|
-
|
|
24
|
-
Scaffold the CMS into a new or existing Next.js project.
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
betterstart init # Interactive setup in current project
|
|
28
|
-
betterstart init my-app # Create new Next.js app + CMS
|
|
29
|
-
betterstart init --preset blog -y # Non-interactive with blog preset
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Presets:**
|
|
33
|
-
|
|
34
|
-
| Preset | What it includes |
|
|
35
|
-
|--------|-----------------|
|
|
36
|
-
| `blank` | Settings singleton only |
|
|
37
|
-
| `blog` | Settings + Categories + Posts (with many-to-many) |
|
|
38
|
-
| `full` | Blog + Navigation + Contact form |
|
|
39
|
-
|
|
40
|
-
Init runs 12 scaffolders in sequence: base directories, tsconfig aliases, Tailwind tokens, env vars, database, auth, UI components (206 files), layouts, API routes, linter config, dependency installation (~130 packages), and preset generation.
|
|
41
|
-
|
|
42
|
-
### `betterstart generate <schema>`
|
|
43
|
-
|
|
44
|
-
Generate everything from a JSON schema file in `cms/schemas/`.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
betterstart generate posts # Entity CRUD (12 files)
|
|
48
|
-
betterstart generate settings # Single-type page (7 files)
|
|
49
|
-
betterstart generate contact # Form + admin (14 files)
|
|
50
|
-
betterstart generate posts --force # Overwrite existing files
|
|
51
|
-
betterstart generate posts --skip-migration # Skip drizzle-kit push
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Schema type is auto-detected: files in `schemas/forms/` or with `submitButtonText`/`steps` are forms, `type: "single"` is a single, everything else is an entity.
|
|
55
|
-
|
|
56
|
-
### `betterstart remove <schema>`
|
|
57
|
-
|
|
58
|
-
Remove all generated files for a schema and clean up `schema.ts` + `navigation.ts`.
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
betterstart remove posts
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### `betterstart seed`
|
|
65
|
-
|
|
66
|
-
Create an initial admin user interactively.
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
betterstart seed
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### `betterstart setup-r2`
|
|
73
|
-
|
|
74
|
-
Configure Cloudflare R2 for file uploads.
|
|
75
|
-
|
|
76
|
-
### `betterstart update-deps`
|
|
77
|
-
|
|
78
|
-
Install or update all CMS dependencies to their latest versions.
|
|
79
|
-
|
|
80
|
-
### `betterstart update-styles`
|
|
81
|
-
|
|
82
|
-
Replace `cms-globals.css` with the latest version from the CLI templates.
|
|
83
|
-
|
|
84
|
-
## Generated file structure
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
cms/
|
|
88
|
-
db/schema.ts # Drizzle table definitions
|
|
89
|
-
lib/actions/<name>.ts # Server actions (CRUD, search, filter, export)
|
|
90
|
-
lib/cache/ # Cache tags, cached queries, revalidation
|
|
91
|
-
hooks/use-<name>.ts # React Query hooks
|
|
92
|
-
components/forms/ # Public form components
|
|
93
|
-
lib/emails/ # React Email notification templates
|
|
94
|
-
data/navigation.ts # Sidebar navigation config
|
|
95
|
-
|
|
96
|
-
app/(cms)/cms/(authenticated)/
|
|
97
|
-
<entity>/
|
|
98
|
-
page.tsx # Server page with Suspense
|
|
99
|
-
columns.tsx # TanStack Table column definitions
|
|
100
|
-
<name>-table.tsx # Data table with pagination + sorting
|
|
101
|
-
page-content.tsx # Client component (search, filters, bulk actions)
|
|
102
|
-
<name>-form.tsx # Create/edit form with Zod validation
|
|
103
|
-
new/page.tsx # Create page
|
|
104
|
-
[id]/edit/page.tsx # Edit page
|
|
105
|
-
forms/<form>/
|
|
106
|
-
page.tsx # Submissions list
|
|
107
|
-
[id]/page.tsx # View submission
|
|
108
|
-
settings/page.tsx # Notification + export settings
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Schema examples
|
|
112
|
-
|
|
113
|
-
### Entity
|
|
114
|
-
|
|
115
|
-
```json
|
|
116
|
-
{
|
|
117
|
-
"name": "posts",
|
|
118
|
-
"label": "Blog Posts",
|
|
119
|
-
"icon": "FileText",
|
|
120
|
-
"navGroup": { "label": "Blog", "icon": "BookOpen" },
|
|
121
|
-
"fields": [
|
|
122
|
-
{ "name": "title", "type": "string", "label": "Title", "required": true },
|
|
123
|
-
{ "name": "slug", "type": "string", "label": "Slug", "hidden": true },
|
|
124
|
-
{ "name": "content", "type": "richtext", "label": "Content", "output": "html" },
|
|
125
|
-
{ "name": "image", "type": "image", "label": "Featured Image" },
|
|
126
|
-
{ "name": "published", "type": "boolean", "label": "Published" },
|
|
127
|
-
{
|
|
128
|
-
"name": "categories",
|
|
129
|
-
"type": "relationship",
|
|
130
|
-
"relationship": "categories",
|
|
131
|
-
"label": "Categories",
|
|
132
|
-
"multiple": true
|
|
133
|
-
}
|
|
134
|
-
],
|
|
135
|
-
"columns": [
|
|
136
|
-
{ "accessorKey": "title", "header": "Title", "type": "text", "sortable": true },
|
|
137
|
-
{ "accessorKey": "published", "header": "Status", "type": "badge" }
|
|
138
|
-
],
|
|
139
|
-
"actions": { "create": true, "edit": true, "delete": true, "draft": true },
|
|
140
|
-
"search": { "fields": ["title"] },
|
|
141
|
-
"autoSlugify": { "enabled": true, "sourceField": "title", "targetField": "slug" }
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Single
|
|
146
|
-
|
|
147
|
-
```json
|
|
148
|
-
{
|
|
149
|
-
"name": "settings",
|
|
150
|
-
"type": "single",
|
|
151
|
-
"label": "Settings",
|
|
152
|
-
"icon": "Settings",
|
|
153
|
-
"fields": [
|
|
154
|
-
{ "name": "siteName", "type": "string", "label": "Site Name" },
|
|
155
|
-
{ "name": "description", "type": "text", "label": "Description" }
|
|
156
|
-
]
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Form (multi-step)
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{
|
|
164
|
-
"name": "candidateApplication",
|
|
165
|
-
"label": "Candidate Applications",
|
|
166
|
-
"icon": "UserPlus",
|
|
167
|
-
"submitButtonText": "Submit Application",
|
|
168
|
-
"successMessage": "Thanks for applying!",
|
|
169
|
-
"notificationEmail": "hr@example.com",
|
|
170
|
-
"steps": [
|
|
171
|
-
{
|
|
172
|
-
"name": "basicInfo",
|
|
173
|
-
"label": "Basic Information",
|
|
174
|
-
"fields": [
|
|
175
|
-
{ "name": "name", "type": "text", "label": "Full Name", "required": true },
|
|
176
|
-
{ "name": "email", "type": "email", "label": "Email", "required": true }
|
|
177
|
-
]
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"name": "experience",
|
|
181
|
-
"label": "Experience",
|
|
182
|
-
"fields": [
|
|
183
|
-
{ "name": "resume", "type": "upload", "label": "Resume" }
|
|
184
|
-
]
|
|
185
|
-
}
|
|
186
|
-
]
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## Field types
|
|
191
|
-
|
|
192
|
-
### Entity/Single (22 types)
|
|
193
|
-
|
|
194
|
-
| Category | Types |
|
|
195
|
-
|----------|-------|
|
|
196
|
-
| Text | `string`, `text`, `varchar` |
|
|
197
|
-
| Rich content | `richtext`, `markdown` |
|
|
198
|
-
| Numbers | `number`, `decimal`, `serial` |
|
|
199
|
-
| Date/time | `date`, `timestamp`, `time` |
|
|
200
|
-
| Boolean | `boolean` |
|
|
201
|
-
| Media | `image`, `video`, `media`, `icon` |
|
|
202
|
-
| Selection | `select`, `relationship` |
|
|
203
|
-
| Layout | `group`, `tabs`, `separator` |
|
|
204
|
-
| Complex | `list`, `curriculum` |
|
|
205
|
-
|
|
206
|
-
### Form (17 types)
|
|
207
|
-
|
|
208
|
-
`text`, `textarea`, `email`, `phone`, `number`, `url`, `date`, `select`, `radio`, `checkbox`, `multiselect`, `file`, `upload`, `group`, `list`, `timezone`, `dynamicFields`
|
|
209
|
-
|
|
210
|
-
## Tech stack
|
|
211
|
-
|
|
212
|
-
| Concern | Technology |
|
|
213
|
-
|---------|-----------|
|
|
214
|
-
| Framework | Next.js 16 (App Router) |
|
|
215
|
-
| Database | Drizzle ORM + Neon PostgreSQL |
|
|
216
|
-
| Auth | Better Auth |
|
|
217
|
-
| UI | Radix primitives + Tailwind CSS v4 |
|
|
218
|
-
| Rich text | TipTap |
|
|
219
|
-
| Markdown | CodeMirror + Shiki + KaTeX |
|
|
220
|
-
| Tables | TanStack React Table |
|
|
221
|
-
| Data fetching | TanStack React Query |
|
|
222
|
-
| Forms | React Hook Form + Zod |
|
|
223
|
-
| URL state | nuqs |
|
|
224
|
-
| Storage | Cloudflare R2 |
|
|
225
|
-
| Email | Resend + React Email (opt-in) |
|
|
226
|
-
| Icons | Lucide React |
|
|
227
|
-
|
|
228
|
-
## Configuration
|
|
229
|
-
|
|
230
|
-
Create `cms.config.ts` in your project root (generated by `init`):
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { defineConfig } from '@betterstart/cli'
|
|
234
|
-
|
|
235
|
-
export default defineConfig({
|
|
236
|
-
database: {
|
|
237
|
-
provider: 'neon',
|
|
238
|
-
},
|
|
239
|
-
features: {
|
|
240
|
-
email: true,
|
|
241
|
-
},
|
|
242
|
-
})
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
All generated code uses `@cms/*` path aliases:
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { db } from '@cms/db'
|
|
249
|
-
import { posts } from '@cms/db/schema'
|
|
250
|
-
import { getPosts } from '@cms/actions/posts'
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## Requirements
|
|
254
|
-
|
|
255
|
-
- Node.js >= 22
|
|
256
|
-
- Next.js 16 with TypeScript and Tailwind CSS v4
|
|
257
|
-
|
|
258
|
-
## License
|
|
259
|
-
|
|
260
|
-
MIT
|
|
3
|
+
> **Warning:** This project is a work in progress. Expect bugs, breaking changes, and incomplete features.
|
package/dist/cli.js
CHANGED
|
@@ -2276,6 +2276,7 @@ function buildDefaultValues(fields) {
|
|
|
2276
2276
|
if (f.type === "checkbox") return ` ${f.name}: false`;
|
|
2277
2277
|
if (f.type === "number") return ` ${f.name}: undefined`;
|
|
2278
2278
|
if (f.type === "multiselect" || f.type === "list") return ` ${f.name}: []`;
|
|
2279
|
+
if (f.type === "radio" && f.defaultValue !== void 0) return ` ${f.name}: '${f.defaultValue}'`;
|
|
2279
2280
|
if (f.type === "select" || f.type === "radio") return ` ${f.name}: undefined`;
|
|
2280
2281
|
if (f.defaultValue !== void 0) return ` ${f.name}: '${f.defaultValue}'`;
|
|
2281
2282
|
return ` ${f.name}: ''`;
|
|
@@ -2343,8 +2344,32 @@ function generateFieldJSX(field) {
|
|
|
2343
2344
|
</FormItem>
|
|
2344
2345
|
)}
|
|
2345
2346
|
/>`;
|
|
2346
|
-
case "select":
|
|
2347
2347
|
case "radio":
|
|
2348
|
+
if (field.options && field.options.length > 0) {
|
|
2349
|
+
const radioItems = field.options.map(
|
|
2350
|
+
(opt) => ` <div className="flex items-center space-x-2">
|
|
2351
|
+
<RadioGroupItem value="${opt.value}" id="${name}-${opt.value}" />
|
|
2352
|
+
<Label htmlFor="${name}-${opt.value}">${escapeJsx(opt.label)}</Label>
|
|
2353
|
+
</div>`
|
|
2354
|
+
).join("\n");
|
|
2355
|
+
return ` <FormField
|
|
2356
|
+
control={form.control}
|
|
2357
|
+
name="${name}"
|
|
2358
|
+
render={({ field }) => (
|
|
2359
|
+
<FormItem className="space-y-3">
|
|
2360
|
+
<FormLabel>${label}${requiredStar}</FormLabel>
|
|
2361
|
+
<FormControl>
|
|
2362
|
+
<RadioGroup onValueChange={field.onChange} defaultValue={field.value} className="flex flex-col space-y-1">
|
|
2363
|
+
${radioItems}
|
|
2364
|
+
</RadioGroup>
|
|
2365
|
+
</FormControl>${hintJSX}
|
|
2366
|
+
<FormMessage />
|
|
2367
|
+
</FormItem>
|
|
2368
|
+
)}
|
|
2369
|
+
/>`;
|
|
2370
|
+
}
|
|
2371
|
+
return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "text");
|
|
2372
|
+
case "select":
|
|
2348
2373
|
if (field.options && field.options.length > 0) {
|
|
2349
2374
|
const optionItems = field.options.map(
|
|
2350
2375
|
(opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
|
|
@@ -2536,21 +2561,27 @@ ${fieldsJSX}
|
|
|
2536
2561
|
}).join("\n");
|
|
2537
2562
|
const submitText = schema.submitButtonText || "Submit";
|
|
2538
2563
|
const successMessage = escapeJsx(schema.successMessage || "Form submitted successfully!");
|
|
2564
|
+
const hasRadio = allFields.some((f) => f.type === "radio");
|
|
2539
2565
|
const buttonImport = resolveUiImport(cwd, "button");
|
|
2540
2566
|
const formImport = resolveUiImport(cwd, "form");
|
|
2541
2567
|
const inputImport = resolveUiImport(cwd, "input");
|
|
2568
|
+
const labelImport = resolveUiImport(cwd, "label");
|
|
2542
2569
|
const textareaImport = resolveUiImport(cwd, "textarea");
|
|
2543
2570
|
const selectImport = resolveUiImport(cwd, "select");
|
|
2571
|
+
const radioGroupImport = resolveUiImport(cwd, "radio-group");
|
|
2544
2572
|
const progressImport = resolveUiImport(cwd, "progress");
|
|
2545
2573
|
const content = buildComponentSource({
|
|
2546
2574
|
pascal,
|
|
2547
2575
|
kebab,
|
|
2548
2576
|
rhfImport,
|
|
2577
|
+
hasRadio,
|
|
2549
2578
|
buttonImport,
|
|
2550
2579
|
formImport,
|
|
2551
2580
|
inputImport,
|
|
2581
|
+
labelImport,
|
|
2552
2582
|
textareaImport,
|
|
2553
2583
|
selectImport,
|
|
2584
|
+
radioGroupImport,
|
|
2554
2585
|
progressImport,
|
|
2555
2586
|
zodFields,
|
|
2556
2587
|
defaults,
|
|
@@ -2598,7 +2629,9 @@ import {
|
|
|
2598
2629
|
FormMessage,
|
|
2599
2630
|
} from '${p7.formImport}'
|
|
2600
2631
|
import { Input } from '${p7.inputImport}'
|
|
2601
|
-
import {
|
|
2632
|
+
import { Label } from '${p7.labelImport}'
|
|
2633
|
+
import { Progress } from '${p7.progressImport}'${p7.hasRadio ? `
|
|
2634
|
+
import { RadioGroup, RadioGroupItem } from '${p7.radioGroupImport}'` : ""}
|
|
2602
2635
|
import { Textarea } from '${p7.textareaImport}'
|
|
2603
2636
|
import {
|
|
2604
2637
|
Select,
|
|
@@ -2792,11 +2825,14 @@ function generateSingleStepForm(schema, cwd, cmsDir, options) {
|
|
|
2792
2825
|
const fieldArraySetup = hasListFields ? `
|
|
2793
2826
|
${buildFieldArrayDecls(listFields)}
|
|
2794
2827
|
` : "";
|
|
2828
|
+
const hasRadio = fields.some((f) => f.type === "radio");
|
|
2795
2829
|
const buttonImport = resolveUiImport(cwd, "button");
|
|
2796
2830
|
const formImport = resolveUiImport(cwd, "form");
|
|
2797
2831
|
const inputImport = resolveUiImport(cwd, "input");
|
|
2832
|
+
const labelImport = resolveUiImport(cwd, "label");
|
|
2798
2833
|
const textareaImport = resolveUiImport(cwd, "textarea");
|
|
2799
2834
|
const selectImport = resolveUiImport(cwd, "select");
|
|
2835
|
+
const radioGroupImport = resolveUiImport(cwd, "radio-group");
|
|
2800
2836
|
const content = `'use client'
|
|
2801
2837
|
|
|
2802
2838
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
@@ -2815,6 +2851,8 @@ import {
|
|
|
2815
2851
|
FormMessage,
|
|
2816
2852
|
} from '${formImport}'
|
|
2817
2853
|
import { Input } from '${inputImport}'
|
|
2854
|
+
import { Label } from '${labelImport}'${hasRadio ? `
|
|
2855
|
+
import { RadioGroup, RadioGroupItem } from '${radioGroupImport}'` : ""}
|
|
2818
2856
|
import { Textarea } from '${textareaImport}'
|
|
2819
2857
|
import {
|
|
2820
2858
|
Select,
|