@betterstart/cli 0.1.38 → 0.1.39
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 +191 -64
- package/dist/cli.js +45 -8
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
1
|
# @betterstart/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Warning:** This project is a work in progress. Expect bugs, breaking changes, and incomplete features.
|
|
4
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @betterstart/cli init
|
|
8
|
+
npx @betterstart/cli init
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## What you get
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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`
|
|
17
19
|
|
|
18
20
|
## Commands
|
|
19
21
|
|
|
20
22
|
### `betterstart init [name]`
|
|
21
23
|
|
|
22
|
-
Scaffold CMS into a new or existing Next.js project.
|
|
24
|
+
Scaffold the CMS into a new or existing Next.js project.
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
betterstart init
|
|
26
|
-
betterstart init my-app
|
|
27
|
-
betterstart init --preset
|
|
28
|
-
betterstart init --preset blog # Blog (default)
|
|
29
|
-
betterstart init --preset full # Blog + navigation + contact form
|
|
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
30
|
```
|
|
31
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
|
+
|
|
32
42
|
### `betterstart generate <schema>`
|
|
33
43
|
|
|
34
|
-
Generate
|
|
44
|
+
Generate everything from a JSON schema file in `cms/schemas/`.
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
|
-
betterstart generate posts
|
|
38
|
-
betterstart generate
|
|
39
|
-
betterstart generate
|
|
40
|
-
betterstart generate posts --
|
|
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
|
|
41
52
|
```
|
|
42
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
|
+
|
|
43
56
|
### `betterstart remove <schema>`
|
|
44
57
|
|
|
45
|
-
Remove all generated files for
|
|
58
|
+
Remove all generated files for a schema and clean up `schema.ts` + `navigation.ts`.
|
|
46
59
|
|
|
47
60
|
```bash
|
|
48
61
|
betterstart remove posts
|
|
@@ -50,83 +63,197 @@ betterstart remove posts
|
|
|
50
63
|
|
|
51
64
|
### `betterstart seed`
|
|
52
65
|
|
|
53
|
-
Create an initial admin user.
|
|
66
|
+
Create an initial admin user interactively.
|
|
54
67
|
|
|
55
68
|
```bash
|
|
56
69
|
betterstart seed
|
|
57
70
|
```
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
### `betterstart setup-r2`
|
|
73
|
+
|
|
74
|
+
Configure Cloudflare R2 for file uploads.
|
|
75
|
+
|
|
76
|
+
### `betterstart update-deps`
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
Install or update all CMS dependencies to their latest versions.
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|---|------|-------------|
|
|
65
|
-
| 1 | `cms/db/schema.ts` | Drizzle table definition (appended) |
|
|
66
|
-
| 2 | `cms/lib/actions/<name>.ts` | Server actions (CRUD) |
|
|
67
|
-
| 3 | `cms/hooks/use-<name>.ts` | React Query hook |
|
|
68
|
-
| 4 | `cms/lib/cache/<name>.ts` | Cache tags + queries |
|
|
69
|
-
| 5 | `app/(cms)/cms/(authenticated)/<name>/columns.tsx` | Table column definitions |
|
|
70
|
-
| 6 | `app/(cms)/cms/(authenticated)/<name>/<name>-table.tsx` | Data table component |
|
|
71
|
-
| 7 | `app/(cms)/cms/(authenticated)/<name>/<name>-page-content.tsx` | Page content (client) |
|
|
72
|
-
| 8 | `app/(cms)/cms/(authenticated)/<name>/page.tsx` | Page (server) |
|
|
73
|
-
| 9 | `app/(cms)/cms/(authenticated)/<name>/<name>-form.tsx` | Create/edit form |
|
|
74
|
-
| 10 | `app/(cms)/cms/(authenticated)/<name>/new/page.tsx` | Create page |
|
|
75
|
-
| 11 | `app/(cms)/cms/(authenticated)/<name>/[id]/edit/page.tsx` | Edit page |
|
|
76
|
-
| 12 | `cms/data/navigation.ts` | Sidebar nav (appended) |
|
|
80
|
+
### `betterstart update-styles`
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
Replace `cms-globals.css` with the latest version from the CLI templates.
|
|
79
83
|
|
|
80
|
-
|
|
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
|
|
81
114
|
|
|
82
115
|
```json
|
|
83
116
|
{
|
|
84
117
|
"name": "posts",
|
|
85
|
-
"label": "Posts",
|
|
118
|
+
"label": "Blog Posts",
|
|
86
119
|
"icon": "FileText",
|
|
120
|
+
"navGroup": { "label": "Blog", "icon": "BookOpen" },
|
|
87
121
|
"fields": [
|
|
88
122
|
{ "name": "title", "type": "string", "label": "Title", "required": true },
|
|
89
|
-
{ "name": "slug", "type": "string", "label": "Slug", "
|
|
90
|
-
{ "name": "content", "type": "richtext", "label": "Content" },
|
|
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" },
|
|
91
126
|
{ "name": "published", "type": "boolean", "label": "Published" },
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
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" }
|
|
95
142
|
}
|
|
96
143
|
```
|
|
97
144
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
### Form Schema
|
|
145
|
+
### Single
|
|
101
146
|
|
|
102
147
|
```json
|
|
103
148
|
{
|
|
104
|
-
"name": "
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
149
|
+
"name": "settings",
|
|
150
|
+
"type": "single",
|
|
151
|
+
"label": "Settings",
|
|
152
|
+
"icon": "Settings",
|
|
108
153
|
"fields": [
|
|
109
|
-
{ "name": "
|
|
110
|
-
{ "name": "
|
|
111
|
-
{ "name": "message", "type": "textarea", "label": "Message", "required": true }
|
|
154
|
+
{ "name": "siteName", "type": "string", "label": "Site Name" },
|
|
155
|
+
{ "name": "description", "type": "text", "label": "Description" }
|
|
112
156
|
]
|
|
113
157
|
}
|
|
114
158
|
```
|
|
115
159
|
|
|
116
|
-
|
|
160
|
+
### Form (multi-step)
|
|
117
161
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
```
|
|
125
252
|
|
|
126
253
|
## Requirements
|
|
127
254
|
|
|
128
255
|
- Node.js >= 22
|
|
129
|
-
- Next.js 16 with TypeScript and Tailwind CSS
|
|
256
|
+
- Next.js 16 with TypeScript and Tailwind CSS v4
|
|
130
257
|
|
|
131
258
|
## License
|
|
132
259
|
|
package/dist/cli.js
CHANGED
|
@@ -2560,6 +2560,38 @@ ${nestedFieldsJSX}
|
|
|
2560
2560
|
// src/generators/form-pipeline/form-database.ts
|
|
2561
2561
|
import fs8 from "fs";
|
|
2562
2562
|
import path8 from "path";
|
|
2563
|
+
function findTableEnd(content, startIndex) {
|
|
2564
|
+
let depth = 0;
|
|
2565
|
+
let inString = false;
|
|
2566
|
+
let stringChar = "";
|
|
2567
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
2568
|
+
const char = content[i];
|
|
2569
|
+
const prev = i > 0 ? content[i - 1] : "";
|
|
2570
|
+
if ((char === '"' || char === "'" || char === "`") && prev !== "\\") {
|
|
2571
|
+
if (!inString) {
|
|
2572
|
+
inString = true;
|
|
2573
|
+
stringChar = char;
|
|
2574
|
+
} else if (char === stringChar) {
|
|
2575
|
+
inString = false;
|
|
2576
|
+
}
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
if (inString) continue;
|
|
2580
|
+
if (char === "(" || char === "{" || char === "[") depth++;
|
|
2581
|
+
if (char === ")" || char === "}" || char === "]") depth--;
|
|
2582
|
+
if (depth === 0 && char === ")") {
|
|
2583
|
+
let end = i + 1;
|
|
2584
|
+
while (end < content.length && (content[end] === ";" || content[end] === " " || content[end] === " ")) {
|
|
2585
|
+
end++;
|
|
2586
|
+
}
|
|
2587
|
+
if (end < content.length && content[end] === "\n") {
|
|
2588
|
+
end++;
|
|
2589
|
+
}
|
|
2590
|
+
return end;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return content.length;
|
|
2594
|
+
}
|
|
2563
2595
|
function generateFormDatabase(schema, cwd, dbSchemaPath, options) {
|
|
2564
2596
|
const tableName = `${toCamelCase(schema.name)}Submissions`;
|
|
2565
2597
|
const fields = getAllFormSchemaFields(schema);
|
|
@@ -2608,8 +2640,13 @@ ${match}`
|
|
|
2608
2640
|
if (!options.force) {
|
|
2609
2641
|
return { files: [dbSchemaPath] };
|
|
2610
2642
|
}
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2643
|
+
const marker = `export const ${tableName} =`;
|
|
2644
|
+
const start = content.indexOf(marker);
|
|
2645
|
+
if (start !== -1) {
|
|
2646
|
+
const end = findTableEnd(content, start);
|
|
2647
|
+
const actualStart = start > 0 && content[start - 1] === "\n" ? start - 1 : start;
|
|
2648
|
+
content = content.slice(0, actualStart) + content.slice(end);
|
|
2649
|
+
}
|
|
2613
2650
|
}
|
|
2614
2651
|
content += `
|
|
2615
2652
|
${tableSchema}`;
|
|
@@ -4813,7 +4850,7 @@ ${updated}`;
|
|
|
4813
4850
|
}
|
|
4814
4851
|
return updated;
|
|
4815
4852
|
}
|
|
4816
|
-
function
|
|
4853
|
+
function findTableEnd2(content, startIndex) {
|
|
4817
4854
|
let depth = 0;
|
|
4818
4855
|
let inString = false;
|
|
4819
4856
|
let stringChar = "";
|
|
@@ -4865,7 +4902,7 @@ function generateDatabase(schema, cwd, schemaDir, options = {}) {
|
|
|
4865
4902
|
let updated = content;
|
|
4866
4903
|
if (options.force && content.includes(`export const ${variableName} =`)) {
|
|
4867
4904
|
const start = content.indexOf(`export const ${variableName} =`);
|
|
4868
|
-
const end =
|
|
4905
|
+
const end = findTableEnd2(content, start);
|
|
4869
4906
|
updated = content.slice(0, start) + tableDef.trim() + content.slice(end);
|
|
4870
4907
|
} else {
|
|
4871
4908
|
updated = `${updated.trimEnd()}
|
|
@@ -4875,7 +4912,7 @@ ${tableDef}`;
|
|
|
4875
4912
|
const jName = junctionNames[i];
|
|
4876
4913
|
if (options.force && updated.includes(`export const ${jName} =`)) {
|
|
4877
4914
|
const start = updated.indexOf(`export const ${jName} =`);
|
|
4878
|
-
const end =
|
|
4915
|
+
const end = findTableEnd2(updated, start);
|
|
4879
4916
|
updated = updated.slice(0, start) + junctionDefs[i].trim() + updated.slice(end);
|
|
4880
4917
|
} else if (!updated.includes(`export const ${jName} =`)) {
|
|
4881
4918
|
updated = `${updated.trimEnd()}
|
|
@@ -14249,7 +14286,7 @@ import fs39 from "fs";
|
|
|
14249
14286
|
import path44 from "path";
|
|
14250
14287
|
import readline from "readline";
|
|
14251
14288
|
import { Command as Command4 } from "commander";
|
|
14252
|
-
function
|
|
14289
|
+
function findTableEnd3(content, startIndex) {
|
|
14253
14290
|
let depth = 0;
|
|
14254
14291
|
let inString = false;
|
|
14255
14292
|
let stringChar = "";
|
|
@@ -14283,7 +14320,7 @@ function removeTableFromSchema(schemaFilePath, name) {
|
|
|
14283
14320
|
let changed = false;
|
|
14284
14321
|
if (content.includes(`export const ${variableName} =`)) {
|
|
14285
14322
|
const start = content.indexOf(`export const ${variableName} =`);
|
|
14286
|
-
const end =
|
|
14323
|
+
const end = findTableEnd3(content, start);
|
|
14287
14324
|
content = content.slice(0, start) + content.slice(end);
|
|
14288
14325
|
changed = true;
|
|
14289
14326
|
}
|
|
@@ -14295,7 +14332,7 @@ function removeTableFromSchema(schemaFilePath, name) {
|
|
|
14295
14332
|
const jVarName = jMatch[1];
|
|
14296
14333
|
const jStart = content.indexOf(`export const ${jVarName} =`);
|
|
14297
14334
|
if (jStart !== -1) {
|
|
14298
|
-
const jEnd =
|
|
14335
|
+
const jEnd = findTableEnd3(content, jStart);
|
|
14299
14336
|
content = content.slice(0, jStart) + content.slice(jEnd);
|
|
14300
14337
|
changed = true;
|
|
14301
14338
|
regex.lastIndex = 0;
|