@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 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 { Progress } from '${p7.progressImport}'
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,