@betterstart/cli 0.1.42 → 0.1.44

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) => ` <label htmlFor="${name}-${opt.value}" className="flex items-center space-x-2 text-sm font-medium leading-none">
2351
+ <RadioGroupItem value="${opt.value}" id="${name}-${opt.value}" />
2352
+ <span>${escapeJsx(opt.label)}</span>
2353
+ </label>`
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 items-center space-x-2">
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,25 @@ ${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");
2542
2568
  const textareaImport = resolveUiImport(cwd, "textarea");
2543
2569
  const selectImport = resolveUiImport(cwd, "select");
2570
+ const radioGroupImport = resolveUiImport(cwd, "radio-group");
2544
2571
  const progressImport = resolveUiImport(cwd, "progress");
2545
2572
  const content = buildComponentSource({
2546
2573
  pascal,
2547
2574
  kebab,
2548
2575
  rhfImport,
2576
+ hasRadio,
2549
2577
  buttonImport,
2550
2578
  formImport,
2551
2579
  inputImport,
2552
2580
  textareaImport,
2553
2581
  selectImport,
2582
+ radioGroupImport,
2554
2583
  progressImport,
2555
2584
  zodFields,
2556
2585
  defaults,
@@ -2598,7 +2627,8 @@ import {
2598
2627
  FormMessage,
2599
2628
  } from '${p7.formImport}'
2600
2629
  import { Input } from '${p7.inputImport}'
2601
- import { Progress } from '${p7.progressImport}'
2630
+ import { Progress } from '${p7.progressImport}'${p7.hasRadio ? `
2631
+ import { RadioGroup, RadioGroupItem } from '${p7.radioGroupImport}'` : ""}
2602
2632
  import { Textarea } from '${p7.textareaImport}'
2603
2633
  import {
2604
2634
  Select,
@@ -2792,11 +2822,13 @@ function generateSingleStepForm(schema, cwd, cmsDir, options) {
2792
2822
  const fieldArraySetup = hasListFields ? `
2793
2823
  ${buildFieldArrayDecls(listFields)}
2794
2824
  ` : "";
2825
+ const hasRadio = fields.some((f) => f.type === "radio");
2795
2826
  const buttonImport = resolveUiImport(cwd, "button");
2796
2827
  const formImport = resolveUiImport(cwd, "form");
2797
2828
  const inputImport = resolveUiImport(cwd, "input");
2798
2829
  const textareaImport = resolveUiImport(cwd, "textarea");
2799
2830
  const selectImport = resolveUiImport(cwd, "select");
2831
+ const radioGroupImport = resolveUiImport(cwd, "radio-group");
2800
2832
  const content = `'use client'
2801
2833
 
2802
2834
  import { zodResolver } from '@hookform/resolvers/zod'
@@ -2814,7 +2846,8 @@ import {
2814
2846
  FormLabel,
2815
2847
  FormMessage,
2816
2848
  } from '${formImport}'
2817
- import { Input } from '${inputImport}'
2849
+ import { Input } from '${inputImport}'${hasRadio ? `
2850
+ import { RadioGroup, RadioGroupItem } from '${radioGroupImport}'` : ""}
2818
2851
  import { Textarea } from '${textareaImport}'
2819
2852
  import {
2820
2853
  Select,