create-better-system 1.0.0
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 +158 -0
- package/index.js +246 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# create-better-system
|
|
2
|
+
|
|
3
|
+
CLI to scaffold new projects from the [Better System](https://github.com/brunogilferro/better-system) template.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
1. Clones the Better System monorepo template
|
|
8
|
+
2. Asks 5 questions about your project
|
|
9
|
+
3. Fills in design tokens, fonts, and CSS variables automatically
|
|
10
|
+
4. Leaves you with a ready-to-code project
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx create-better-system
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g create-better-system
|
|
24
|
+
create-better-system
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Questions asked
|
|
30
|
+
|
|
31
|
+
| Question | Default | Notes |
|
|
32
|
+
|----------|---------|-------|
|
|
33
|
+
| Project name | — | Used as folder name and package name |
|
|
34
|
+
| Color theme | Dark only | Dark / Light / Both (with toggle) |
|
|
35
|
+
| Heading font | Poppins | Any Google Font name |
|
|
36
|
+
| Primary accent color | `#D4AF37` | Main brand color |
|
|
37
|
+
| Secondary accent color | `#38bdf8` | Secondary highlights |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## After creation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 1. Enter the project folder
|
|
45
|
+
cd <project-name>
|
|
46
|
+
|
|
47
|
+
# 2. Install dependencies
|
|
48
|
+
pnpm install
|
|
49
|
+
|
|
50
|
+
# 3. Set up backend environment
|
|
51
|
+
cp apps/backend/.env.example apps/backend/.env
|
|
52
|
+
# Edit apps/backend/.env and set APP_KEY and any other required values
|
|
53
|
+
|
|
54
|
+
# 4. Run database migrations
|
|
55
|
+
cd apps/backend
|
|
56
|
+
node ace migration:run
|
|
57
|
+
|
|
58
|
+
# 5. Start the project
|
|
59
|
+
cd ../..
|
|
60
|
+
pnpm dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Frontend runs on `http://localhost:3000`
|
|
64
|
+
Backend runs on `http://localhost:3333`
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## What gets configured automatically
|
|
69
|
+
|
|
70
|
+
- `docs/figma-design-rules.md` — filled with your design tokens
|
|
71
|
+
- `apps/frontend/app/globals.css` — CSS variables set from your tokens
|
|
72
|
+
- `apps/frontend/app/layout.tsx` — heading font imported from `next/font/google`
|
|
73
|
+
- `package.json` — project name updated
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Using AI in your project
|
|
78
|
+
|
|
79
|
+
Once the project is open in Cursor, the AI already knows your architecture, design tokens, and rules.
|
|
80
|
+
|
|
81
|
+
### Create a complete feature (backend + frontend)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
new-feature: courses with name, description and price
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The AI will generate the migration, model, validator, service, controller, route, types, hook, and page automatically.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Implement a screen from Figma
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
figma-to-next-screen: [paste Figma link here]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The AI reads the design, creates an architecture plan, implements with shadcn + your design tokens, and refines against the Figma screenshot.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Generate only the backend for a resource (new table)
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
backend-from-schema: product with name (string), price (number), stock (integer)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Generates migration, model, validator, transformer, service, controller, and route.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Generate backend from an existing database table
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# First, pull the current schema from the database
|
|
115
|
+
cd apps/backend && node ace db:pull
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then in Cursor:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
database-to-feature: tabela products
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Generates model (enhanced from db:pull), validator, transformer, service, controller, and route. **No migration created** — the table already exists.
|
|
125
|
+
|
|
126
|
+
For tables that already exist, you can also use:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
backend-from-schema: product [existing table] with name, price, stock
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Write tests
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
write-tests: apps/backend/app/services/product_service.ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Generates unit and integration tests following the project's conventions.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Template includes
|
|
145
|
+
|
|
146
|
+
- **Frontend**: Next.js 16, React 19, Tailwind v4, shadcn/ui (14 components), React Query
|
|
147
|
+
- **Backend**: AdonisJS v7 with auth (signup, login, logout, profile)
|
|
148
|
+
- **Type-safe API**: Tuyau client pre-configured in `lib/api.ts`
|
|
149
|
+
- **Cursor AI skills**: `figma-to-next-screen`, `backend-from-schema`, `new-feature`, `write-tests`
|
|
150
|
+
- **Cursor AI rules**: frontend, backend, accessibility, commits
|
|
151
|
+
- **Docs**: design-system, components-registry, data-pattern, api-conventions, error-handling, state-management, figma-design-rules
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Requirements
|
|
156
|
+
|
|
157
|
+
- Node.js 18+
|
|
158
|
+
- pnpm
|
package/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { execa } from 'execa'
|
|
5
|
+
import fs from 'fs-extra'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
|
|
8
|
+
const THEME_PRESETS = {
|
|
9
|
+
dark: {
|
|
10
|
+
bgPrimary: '#0a0e16',
|
|
11
|
+
bgSecondary: '#0e1117',
|
|
12
|
+
bgSurface: '#111827',
|
|
13
|
+
textPrimary: '#e0e0d0',
|
|
14
|
+
textSecondary: '#6a7282',
|
|
15
|
+
borderPrimary: '#1a2233',
|
|
16
|
+
borderSecondary: '#2a3545',
|
|
17
|
+
},
|
|
18
|
+
light: {
|
|
19
|
+
bgPrimary: '#ffffff',
|
|
20
|
+
bgSecondary: '#f9fafb',
|
|
21
|
+
bgSurface: '#f3f4f6',
|
|
22
|
+
textPrimary: '#111827',
|
|
23
|
+
textSecondary: '#6b7280',
|
|
24
|
+
borderPrimary: '#e5e7eb',
|
|
25
|
+
borderSecondary: '#d1d5db',
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Converts a font name like "Open Sans" to the Next.js import format "Open_Sans"
|
|
30
|
+
function toNextFontName(fontName) {
|
|
31
|
+
return fontName.trim().replace(/\s+/g, '_')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Converts a font name to a valid JS variable name: "Open Sans" → "openSans"
|
|
35
|
+
function toVarName(fontName) {
|
|
36
|
+
const words = fontName.trim().split(/\s+/)
|
|
37
|
+
return words
|
|
38
|
+
.map((w, i) => (i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()))
|
|
39
|
+
.join('')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function colorTable(colors) {
|
|
43
|
+
return [
|
|
44
|
+
'| Token | Value | Usage |',
|
|
45
|
+
'|----------------------|----------------------|----------------------|',
|
|
46
|
+
`| \`--bg-primary\` | ${colors.bgPrimary.padEnd(20)} | Main page background |`,
|
|
47
|
+
`| \`--bg-secondary\` | ${colors.bgSecondary.padEnd(20)} | Secondary surfaces |`,
|
|
48
|
+
`| \`--bg-surface\` | ${colors.bgSurface.padEnd(20)} | Cards, panels |`,
|
|
49
|
+
`| \`--text-primary\` | ${colors.textPrimary.padEnd(20)} | Primary text |`,
|
|
50
|
+
`| \`--text-secondary\` | ${colors.textSecondary.padEnd(20)} | Muted text |`,
|
|
51
|
+
`| \`--border-primary\` | ${colors.borderPrimary.padEnd(20)} | Default borders |`,
|
|
52
|
+
`| \`--border-secondary\` | ${colors.borderSecondary.padEnd(20)} | Subtle dividers |`,
|
|
53
|
+
].join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Prompts ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
console.log('\n🚀 Better System — Create new project\n')
|
|
59
|
+
|
|
60
|
+
const answers = await inquirer.prompt([
|
|
61
|
+
{
|
|
62
|
+
name: 'name',
|
|
63
|
+
message: 'Project name?',
|
|
64
|
+
validate: (v) => v.trim().length > 0 || 'Name is required',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'theme',
|
|
68
|
+
type: 'list',
|
|
69
|
+
message: 'Color theme?',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: 'Dark only', value: 'dark' },
|
|
72
|
+
{ name: 'Light only', value: 'light' },
|
|
73
|
+
{ name: 'Both (dark + light with toggle)', value: 'both' },
|
|
74
|
+
],
|
|
75
|
+
default: 'dark',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'headingFont',
|
|
79
|
+
message: 'Heading font (e.g. Poppins, Sora)?',
|
|
80
|
+
default: 'Poppins',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'accentPrimary',
|
|
84
|
+
message: 'Primary accent color (hex)?',
|
|
85
|
+
default: '#D4AF37',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'accentSecondary',
|
|
89
|
+
message: 'Secondary accent color (hex)?',
|
|
90
|
+
default: '#38bdf8',
|
|
91
|
+
},
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
// ─── Resolve values ───────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const projectName = answers.name.trim()
|
|
97
|
+
const projectPath = path.resolve(process.cwd(), projectName)
|
|
98
|
+
|
|
99
|
+
const isBoth = answers.theme === 'both'
|
|
100
|
+
const isDark = answers.theme === 'dark'
|
|
101
|
+
const rootColors = isBoth ? THEME_PRESETS.light : (isDark ? THEME_PRESETS.dark : THEME_PRESETS.light)
|
|
102
|
+
const darkColors = THEME_PRESETS.dark
|
|
103
|
+
|
|
104
|
+
// Font helpers
|
|
105
|
+
const headingFontNextName = toNextFontName(answers.headingFont)
|
|
106
|
+
const headingFontVarName = toVarName(answers.headingFont) + 'Font'
|
|
107
|
+
|
|
108
|
+
// ─── Clone template ───────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
console.log('\n📦 Cloning Better System template...')
|
|
111
|
+
await execa('npx', ['degit', 'brunogilferro/better-system', projectName], {
|
|
112
|
+
stdio: 'inherit',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ─── Update layout.tsx with heading font ─────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
console.log('🔤 Configuring fonts...')
|
|
118
|
+
|
|
119
|
+
const layoutPath = path.join(projectPath, 'apps', 'frontend', 'app', 'layout.tsx')
|
|
120
|
+
let layout = await fs.readFile(layoutPath, 'utf-8')
|
|
121
|
+
|
|
122
|
+
layout = layout
|
|
123
|
+
.replace(
|
|
124
|
+
'// __HEADING_FONT_IMPORT__',
|
|
125
|
+
`import { ${headingFontNextName} } from "next/font/google"`
|
|
126
|
+
)
|
|
127
|
+
.replace(
|
|
128
|
+
'// __HEADING_FONT_DECLARATION__',
|
|
129
|
+
`const ${headingFontVarName} = ${headingFontNextName}({\n variable: "--font-heading",\n subsets: ["latin"],\n weight: ["400", "500", "700"],\n})`
|
|
130
|
+
)
|
|
131
|
+
.replaceAll('__HEADING_FONT_CLASS__', `\${${headingFontVarName}.variable}`)
|
|
132
|
+
.replaceAll('__PROJECT_NAME__', projectName)
|
|
133
|
+
|
|
134
|
+
await fs.writeFile(layoutPath, layout)
|
|
135
|
+
|
|
136
|
+
// ─── Fill globals.css placeholders ───────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
console.log('💅 Configuring CSS variables...')
|
|
139
|
+
|
|
140
|
+
const cssPath = path.join(projectPath, 'apps', 'frontend', 'app', 'globals.css')
|
|
141
|
+
let css = await fs.readFile(cssPath, 'utf-8')
|
|
142
|
+
|
|
143
|
+
const cssReplacements = {
|
|
144
|
+
'__HEADING_FONT__': answers.headingFont,
|
|
145
|
+
'__BODY_FONT__': 'Inter',
|
|
146
|
+
'__ACCENT_PRIMARY__': answers.accentPrimary,
|
|
147
|
+
'__ACCENT_PRIMARY_HOVER__': answers.accentPrimary,
|
|
148
|
+
'__ACCENT_SECONDARY__': answers.accentSecondary,
|
|
149
|
+
'__BG_PRIMARY__': rootColors.bgPrimary,
|
|
150
|
+
'__BG_SECONDARY__': rootColors.bgSecondary,
|
|
151
|
+
'__BG_SURFACE__': rootColors.bgSurface,
|
|
152
|
+
'__TEXT_PRIMARY__': rootColors.textPrimary,
|
|
153
|
+
'__TEXT_SECONDARY__': rootColors.textSecondary,
|
|
154
|
+
'__BORDER_PRIMARY__': rootColors.borderPrimary,
|
|
155
|
+
'__BORDER_SECONDARY__': rootColors.borderSecondary,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const [placeholder, value] of Object.entries(cssReplacements)) {
|
|
159
|
+
css = css.replaceAll(placeholder, value)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isBoth) {
|
|
163
|
+
const darkReplacements = {
|
|
164
|
+
'__DARK_BG_PRIMARY__': darkColors.bgPrimary,
|
|
165
|
+
'__DARK_BG_SECONDARY__': darkColors.bgSecondary,
|
|
166
|
+
'__DARK_BG_SURFACE__': darkColors.bgSurface,
|
|
167
|
+
'__DARK_TEXT_PRIMARY__': darkColors.textPrimary,
|
|
168
|
+
'__DARK_TEXT_SECONDARY__': darkColors.textSecondary,
|
|
169
|
+
'__DARK_BORDER_PRIMARY__': darkColors.borderPrimary,
|
|
170
|
+
'__DARK_BORDER_SECONDARY__': darkColors.borderSecondary,
|
|
171
|
+
}
|
|
172
|
+
for (const [placeholder, value] of Object.entries(darkReplacements)) {
|
|
173
|
+
css = css.replaceAll(placeholder, value)
|
|
174
|
+
}
|
|
175
|
+
css = css.replace('/* DARK_THEME_BLOCK_START */', '').replace('/* DARK_THEME_BLOCK_END */', '')
|
|
176
|
+
} else {
|
|
177
|
+
css = css.replace(
|
|
178
|
+
/\/\* DARK_THEME_BLOCK_START \*\/[\s\S]*?\/\* DARK_THEME_BLOCK_END \*\//,
|
|
179
|
+
''
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await fs.writeFile(cssPath, css)
|
|
184
|
+
|
|
185
|
+
// ─── Fill figma-design-rules.md placeholders ─────────────────────────────────
|
|
186
|
+
|
|
187
|
+
console.log('🎨 Configuring design tokens...')
|
|
188
|
+
|
|
189
|
+
const figmaRulesPath = path.join(projectPath, 'docs', 'figma-design-rules.md')
|
|
190
|
+
let figmaRules = await fs.readFile(figmaRulesPath, 'utf-8')
|
|
191
|
+
|
|
192
|
+
let colorsSection
|
|
193
|
+
if (isBoth) {
|
|
194
|
+
colorsSection = [
|
|
195
|
+
'## Colors — Light theme (default)',
|
|
196
|
+
'',
|
|
197
|
+
colorTable(THEME_PRESETS.light),
|
|
198
|
+
'',
|
|
199
|
+
'## Colors — Dark theme (`.dark` class on `<html>`)',
|
|
200
|
+
'',
|
|
201
|
+
colorTable(THEME_PRESETS.dark),
|
|
202
|
+
].join('\n')
|
|
203
|
+
} else {
|
|
204
|
+
const label = isDark ? 'Dark theme' : 'Light theme'
|
|
205
|
+
colorsSection = `## Colors — ${label}\n\n${colorTable(rootColors)}`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const figmaReplacements = {
|
|
209
|
+
'__THEME_MODE__': answers.theme,
|
|
210
|
+
'__HEADING_FONT__': answers.headingFont,
|
|
211
|
+
'__BODY_FONT__': 'Inter',
|
|
212
|
+
'__ACCENT_PRIMARY__': answers.accentPrimary,
|
|
213
|
+
'__ACCENT_PRIMARY_HOVER__': answers.accentPrimary,
|
|
214
|
+
'__ACCENT_SECONDARY__': answers.accentSecondary,
|
|
215
|
+
'__COLORS_SECTION__': colorsSection,
|
|
216
|
+
'__CSS_BLOCK__': isBoth ? 'Both light and dark — see globals.css' : `Single theme (${answers.theme})`,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const [placeholder, value] of Object.entries(figmaReplacements)) {
|
|
220
|
+
figmaRules = figmaRules.replaceAll(placeholder, value)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fs.writeFile(figmaRulesPath, figmaRules)
|
|
224
|
+
|
|
225
|
+
// ─── Update root package.json ─────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
console.log('📝 Updating package.json...')
|
|
228
|
+
|
|
229
|
+
const rootPkgPath = path.join(projectPath, 'package.json')
|
|
230
|
+
const rootPkg = await fs.readJson(rootPkgPath)
|
|
231
|
+
rootPkg.name = projectName
|
|
232
|
+
await fs.writeJson(rootPkgPath, rootPkg, { spaces: 2 })
|
|
233
|
+
|
|
234
|
+
// ─── Done ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
console.log(`\n✅ Project "${projectName}" created!`)
|
|
237
|
+
|
|
238
|
+
if (isBoth) {
|
|
239
|
+
console.log(`\n💡 Both themes enabled.`)
|
|
240
|
+
console.log(` Light is the default. Add class="dark" to <html> to activate dark.`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(`\nNext steps:`)
|
|
244
|
+
console.log(` cd ${projectName}`)
|
|
245
|
+
console.log(` pnpm install`)
|
|
246
|
+
console.log(` pnpm dev\n`)
|
package/package.json
ADDED