create-ej-app 30.0.0 → 32.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 +21 -1
- package/cli.js +169 -35
- package/cli.ts +213 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,8 @@ You will be prompted for:
|
|
|
59
59
|
- Project name
|
|
60
60
|
- Description
|
|
61
61
|
- Template (`nextjs`, `saas-starter`, or `api`)
|
|
62
|
-
- Whether to initialize git
|
|
62
|
+
- Whether to initialize git. If yes, the CLI creates the `main` branch (or your configured default, unless it is `master`) and makes the initial commit, as long as `.env` is ignored.
|
|
63
|
+
- Whether to update packages to their latest versions. With Bun it runs `bun update -i`, so you pick which packages to update.
|
|
63
64
|
|
|
64
65
|
Example:
|
|
65
66
|
|
|
@@ -68,8 +69,27 @@ Example:
|
|
|
68
69
|
? Enter a description for the project: Production-ready Next.js app
|
|
69
70
|
? Select a template: nextjs
|
|
70
71
|
? Initialize a git repository? yes
|
|
72
|
+
? Update packages to their latest versions? The template can lag behind. yes
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
### Without Prompts
|
|
76
|
+
|
|
77
|
+
Pass the answers as flags to skip the prompts, for example in scripts, CI or coding agents. Any answer you leave out is still asked:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bunx create-ej-app@latest --name my-app --description "Production-ready Next.js app" --template nextjs --git --no-update
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Flag | Answers |
|
|
84
|
+
| -------------------------------- | --------------------------------------------- |
|
|
85
|
+
| `-n, --name <name>` | Project name, also the folder name |
|
|
86
|
+
| `-d, --description <text>` | Description |
|
|
87
|
+
| `-t, --template <template>` | Template: `nextjs`, `saas-starter`, or `api` |
|
|
88
|
+
| `--git` or `--no-git` | Whether to initialize git and commit |
|
|
89
|
+
| `--update` or `--no-update` | Whether to update packages to latest versions |
|
|
90
|
+
|
|
91
|
+
Without a terminal, the CLI can't ask, so it exits and names the flags you still need to pass. `--update` without a terminal updates every package to its latest version, which can include major versions: run the type-check and tests afterwards.
|
|
92
|
+
|
|
73
93
|
---
|
|
74
94
|
|
|
75
95
|
## What you get
|
package/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
2
|
+
import { execFileSync, execSync } from 'child_process';
|
|
3
3
|
import { program } from 'commander';
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
5
|
import enquirer from 'enquirer';
|
|
@@ -48,54 +48,167 @@ const getInstallCommand = (packageManager) => {
|
|
|
48
48
|
return 'npm i';
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Commands that update dependencies to their latest versions: `interactive`
|
|
53
|
+
* lets you pick packages in a terminal, `all` updates everything. Only Bun and
|
|
54
|
+
* pnpm have both; other package managers get instructions instead.
|
|
55
|
+
*/
|
|
56
|
+
const getUpdateCommands = (packageManager) => {
|
|
57
|
+
switch (packageManager) {
|
|
58
|
+
case 'bun':
|
|
59
|
+
return { interactive: 'bun update -i', all: 'bun update --latest' };
|
|
60
|
+
case 'pnpm':
|
|
61
|
+
return {
|
|
62
|
+
interactive: 'pnpm update -i --latest',
|
|
63
|
+
all: 'pnpm update --latest'
|
|
64
|
+
};
|
|
65
|
+
default:
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The branch name for a new repository: the one set in git's
|
|
71
|
+
* init.defaultBranch, unless that is unset or `master`, which become `main`.
|
|
72
|
+
*/
|
|
73
|
+
const defaultBranch = () => {
|
|
74
|
+
let configured = '';
|
|
75
|
+
try {
|
|
76
|
+
configured = execSync('git config --get init.defaultBranch', {
|
|
77
|
+
stdio: 'pipe'
|
|
78
|
+
})
|
|
79
|
+
.toString()
|
|
80
|
+
.trim();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Not set
|
|
84
|
+
}
|
|
85
|
+
return configured && configured !== 'master' ? configured : 'main';
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Commits the new project once. Skips the commit when .env is not ignored, so
|
|
89
|
+
* the generated secret never lands in history, and explains a missing git
|
|
90
|
+
* identity instead of failing silently.
|
|
91
|
+
*/
|
|
92
|
+
const commitEverything = (projectDir) => {
|
|
93
|
+
const run = (command) => execSync(command, { cwd: projectDir, stdio: 'pipe' });
|
|
94
|
+
try {
|
|
95
|
+
run('git check-ignore -q .env');
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.error('Skipped the initial commit: .env is not in .gitignore, and committing it would publish its secrets.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
run('git add -A');
|
|
103
|
+
run('git commit -q -m "initial commit: bootstrap new project with create-ej-app"');
|
|
104
|
+
console.log('Created the initial commit.');
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
console.error(/user\.(name|email)|identity/i.test(message)
|
|
109
|
+
? "Skipped the initial commit: git doesn't know who you are. Set git config user.name and user.email, then run 'git add -A && git commit'."
|
|
110
|
+
: `The initial commit failed: ${message}`);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
51
113
|
const getRunCommand = (packageManager, script) => packageManager === 'npm'
|
|
52
114
|
? `npm run ${script}`
|
|
53
115
|
: `${packageManager} run ${script}`;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
116
|
+
// The name becomes a directory and the package.json name, so keep it to
|
|
117
|
+
// characters that are safe for both.
|
|
118
|
+
const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/;
|
|
119
|
+
const PROJECT_NAME_HINT = 'Use lowercase letters, numbers, dots, hyphens, and underscores.';
|
|
120
|
+
const TEMPLATES = [
|
|
121
|
+
{
|
|
122
|
+
name: 'nextjs',
|
|
123
|
+
message: 'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'saas-starter',
|
|
127
|
+
message: 'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'api',
|
|
131
|
+
message: 'API server with Elysia, Drizzle, and TypeScript'
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
program
|
|
135
|
+
.name('create-ej-app')
|
|
136
|
+
.description('Create a new app from a template. Flags answer the prompts; anything left out is asked.')
|
|
137
|
+
.option('-n, --name <name>', 'project name, also the folder name')
|
|
138
|
+
.option('-d, --description <text>', 'project description')
|
|
139
|
+
.option('-t, --template <template>', `template: ${TEMPLATES.map(t => t.name).join(', ')}`)
|
|
140
|
+
.option('--git', 'initialize a git repository')
|
|
141
|
+
.option('--no-git', 'skip git initialization')
|
|
142
|
+
.option('--update', 'update packages to their latest versions after creating the project')
|
|
143
|
+
.option('--no-update', 'keep the template package versions');
|
|
144
|
+
program.action(async (options) => {
|
|
145
|
+
if (options.name !== undefined && !PROJECT_NAME_PATTERN.test(options.name)) {
|
|
146
|
+
console.error(`Invalid project name '${options.name}'. ${PROJECT_NAME_HINT}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (options.template !== undefined &&
|
|
150
|
+
!TEMPLATES.some(t => t.name === options.template)) {
|
|
151
|
+
console.error(`Unknown template '${options.template}'. Use one of: ${TEMPLATES.map(t => t.name).join(', ')}.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const questions = [
|
|
155
|
+
options.name === undefined && {
|
|
57
156
|
type: 'input',
|
|
58
157
|
name: 'projectName',
|
|
59
158
|
message: 'Enter the project name:',
|
|
60
159
|
initial: 'my-project',
|
|
61
|
-
|
|
62
|
-
// to characters that are safe for both.
|
|
63
|
-
validate: (value) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(value) ||
|
|
64
|
-
'Use lowercase letters, numbers, dots, hyphens, and underscores.'
|
|
160
|
+
validate: (value) => PROJECT_NAME_PATTERN.test(value) || PROJECT_NAME_HINT
|
|
65
161
|
},
|
|
66
|
-
{
|
|
162
|
+
options.description === undefined && {
|
|
67
163
|
type: 'input',
|
|
68
164
|
name: 'description',
|
|
69
165
|
message: 'Enter a description for the project:',
|
|
70
166
|
initial: 'A Legendary Project'
|
|
71
167
|
},
|
|
72
|
-
{
|
|
168
|
+
options.template === undefined && {
|
|
73
169
|
type: 'select',
|
|
74
170
|
name: 'template',
|
|
75
171
|
message: 'Select a template:',
|
|
76
|
-
choices:
|
|
77
|
-
{
|
|
78
|
-
name: 'nextjs',
|
|
79
|
-
message: 'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'saas-starter',
|
|
83
|
-
message: 'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'api',
|
|
87
|
-
message: 'API server with Elysia, Drizzle, and TypeScript'
|
|
88
|
-
}
|
|
89
|
-
]
|
|
172
|
+
choices: TEMPLATES
|
|
90
173
|
},
|
|
91
|
-
{
|
|
174
|
+
options.git === undefined && {
|
|
92
175
|
type: 'select',
|
|
93
176
|
name: 'git',
|
|
94
177
|
message: 'Initialize a git repository?',
|
|
95
178
|
choices: ['yes', 'no']
|
|
179
|
+
},
|
|
180
|
+
options.update === undefined && {
|
|
181
|
+
type: 'select',
|
|
182
|
+
name: 'update',
|
|
183
|
+
message: 'Update packages to their latest versions? The template can lag behind.',
|
|
184
|
+
choices: ['yes', 'no']
|
|
96
185
|
}
|
|
97
|
-
]);
|
|
98
|
-
|
|
186
|
+
].filter(q => q !== false);
|
|
187
|
+
// Without a terminal the prompts would wait forever, for example when an
|
|
188
|
+
// agent or a script runs the CLI, so name the missing flags instead.
|
|
189
|
+
if (questions.length && !process.stdin.isTTY) {
|
|
190
|
+
const flags = {
|
|
191
|
+
projectName: '--name',
|
|
192
|
+
description: '--description',
|
|
193
|
+
template: '--template',
|
|
194
|
+
git: '--git or --no-git',
|
|
195
|
+
update: '--update or --no-update'
|
|
196
|
+
};
|
|
197
|
+
console.error(`No terminal to ask questions in. Pass ${questions.map(q => flags[q.name]).join(', ')}.`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const answers = questions.length
|
|
201
|
+
? await enquirer.prompt(questions)
|
|
202
|
+
: {};
|
|
203
|
+
const projectName = options.name ?? answers.projectName;
|
|
204
|
+
const description = options.description ?? answers.description;
|
|
205
|
+
const template = options.template ?? answers.template;
|
|
206
|
+
const git = options.git === undefined ? answers.git : options.git ? 'yes' : 'no';
|
|
207
|
+
const update = options.update === undefined
|
|
208
|
+
? answers.update
|
|
209
|
+
: options.update
|
|
210
|
+
? 'yes'
|
|
211
|
+
: 'no';
|
|
99
212
|
const projectDir = path.join(process.cwd(), projectName);
|
|
100
213
|
if (fs.existsSync(projectDir)) {
|
|
101
214
|
console.error(`Directory '${projectName}' already exists. Choose another project name or remove the directory.`);
|
|
@@ -146,18 +259,41 @@ program.action(async () => {
|
|
|
146
259
|
if (!fs.existsSync(readmeFile)) {
|
|
147
260
|
fs.writeFileSync(readmeFile, `# ${projectName}\n\n${description}\n`);
|
|
148
261
|
}
|
|
262
|
+
const pm = detectPackageManager();
|
|
263
|
+
const installCommand = getInstallCommand(pm);
|
|
264
|
+
const devCommand = getRunCommand(pm, 'dev');
|
|
265
|
+
const dbPushCommand = getRunCommand(pm, 'db:push');
|
|
266
|
+
// Updating also installs, so the initial commit includes the lockfile
|
|
267
|
+
let installed = false;
|
|
268
|
+
if (update === 'yes') {
|
|
269
|
+
const commands = getUpdateCommands(pm);
|
|
270
|
+
if (!commands) {
|
|
271
|
+
console.log(`\nAutomatic updates support Bun and pnpm. After '${installCommand}', update packages with your package manager.`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const command = process.stdin.isTTY ? commands.interactive : commands.all;
|
|
275
|
+
console.log(`\nUpdating packages with '${command}'…`);
|
|
276
|
+
try {
|
|
277
|
+
execSync(command, { cwd: projectDir, stdio: 'inherit' });
|
|
278
|
+
installed = true;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
console.error(`Updating packages failed, so the template versions stay. Run '${command}' in the project to try again.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
149
285
|
if (git === 'yes') {
|
|
150
286
|
try {
|
|
151
|
-
|
|
287
|
+
execFileSync('git', ['init', '-b', defaultBranch()], {
|
|
288
|
+
cwd: projectDir,
|
|
289
|
+
stdio: 'inherit'
|
|
290
|
+
});
|
|
291
|
+
commitEverything(projectDir);
|
|
152
292
|
}
|
|
153
293
|
catch (err) {
|
|
154
294
|
console.error('Error initializing git repository:', err);
|
|
155
295
|
}
|
|
156
296
|
}
|
|
157
|
-
const pm = detectPackageManager();
|
|
158
|
-
const installCommand = getInstallCommand(pm);
|
|
159
|
-
const devCommand = getRunCommand(pm, 'dev');
|
|
160
|
-
const dbPushCommand = getRunCommand(pm, 'db:push');
|
|
161
297
|
console.log(`
|
|
162
298
|
Created a new app called '${projectName}' 🎉
|
|
163
299
|
|
|
@@ -168,9 +304,7 @@ code ${projectName}
|
|
|
168
304
|
|
|
169
305
|
To get started, run these commands:
|
|
170
306
|
cd ${projectName}
|
|
171
|
-
|
|
172
|
-
${installCommand}
|
|
173
|
-
|
|
307
|
+
${installed ? '' : `\n${installCommand}\n`}
|
|
174
308
|
Fill in the values in .env, then create the database tables:
|
|
175
309
|
|
|
176
310
|
${dbPushCommand}
|
package/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync } from 'child_process'
|
|
3
|
+
import { execFileSync, execSync } from 'child_process'
|
|
4
4
|
import { program } from 'commander'
|
|
5
5
|
import { randomBytes } from 'crypto'
|
|
6
6
|
import enquirer from 'enquirer'
|
|
@@ -15,6 +15,7 @@ interface Response {
|
|
|
15
15
|
description: string
|
|
16
16
|
template: string
|
|
17
17
|
git: string
|
|
18
|
+
update: string
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -61,60 +62,210 @@ const getInstallCommand = (packageManager: string) => {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Commands that update dependencies to their latest versions: `interactive`
|
|
67
|
+
* lets you pick packages in a terminal, `all` updates everything. Only Bun and
|
|
68
|
+
* pnpm have both; other package managers get instructions instead.
|
|
69
|
+
*/
|
|
70
|
+
const getUpdateCommands = (packageManager: string) => {
|
|
71
|
+
switch (packageManager) {
|
|
72
|
+
case 'bun':
|
|
73
|
+
return { interactive: 'bun update -i', all: 'bun update --latest' }
|
|
74
|
+
case 'pnpm':
|
|
75
|
+
return {
|
|
76
|
+
interactive: 'pnpm update -i --latest',
|
|
77
|
+
all: 'pnpm update --latest'
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The branch name for a new repository: the one set in git's
|
|
86
|
+
* init.defaultBranch, unless that is unset or `master`, which become `main`.
|
|
87
|
+
*/
|
|
88
|
+
const defaultBranch = () => {
|
|
89
|
+
let configured = ''
|
|
90
|
+
try {
|
|
91
|
+
configured = execSync('git config --get init.defaultBranch', {
|
|
92
|
+
stdio: 'pipe'
|
|
93
|
+
})
|
|
94
|
+
.toString()
|
|
95
|
+
.trim()
|
|
96
|
+
} catch {
|
|
97
|
+
// Not set
|
|
98
|
+
}
|
|
99
|
+
return configured && configured !== 'master' ? configured : 'main'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Commits the new project once. Skips the commit when .env is not ignored, so
|
|
104
|
+
* the generated secret never lands in history, and explains a missing git
|
|
105
|
+
* identity instead of failing silently.
|
|
106
|
+
*/
|
|
107
|
+
const commitEverything = (projectDir: string) => {
|
|
108
|
+
const run = (command: string) =>
|
|
109
|
+
execSync(command, { cwd: projectDir, stdio: 'pipe' })
|
|
110
|
+
try {
|
|
111
|
+
run('git check-ignore -q .env')
|
|
112
|
+
} catch {
|
|
113
|
+
console.error(
|
|
114
|
+
'Skipped the initial commit: .env is not in .gitignore, and committing it would publish its secrets.'
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
run('git add -A')
|
|
120
|
+
run('git commit -q -m "initial commit: bootstrap new project with create-ej-app"')
|
|
121
|
+
console.log('Created the initial commit.')
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
124
|
+
console.error(
|
|
125
|
+
/user\.(name|email)|identity/i.test(message)
|
|
126
|
+
? "Skipped the initial commit: git doesn't know who you are. Set git config user.name and user.email, then run 'git add -A && git commit'."
|
|
127
|
+
: `The initial commit failed: ${message}`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
64
132
|
const getRunCommand = (packageManager: string, script: string) =>
|
|
65
133
|
packageManager === 'npm'
|
|
66
134
|
? `npm run ${script}`
|
|
67
135
|
: `${packageManager} run ${script}`
|
|
68
136
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
// The name becomes a directory and the package.json name, so keep it to
|
|
138
|
+
// characters that are safe for both.
|
|
139
|
+
const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/
|
|
140
|
+
const PROJECT_NAME_HINT =
|
|
141
|
+
'Use lowercase letters, numbers, dots, hyphens, and underscores.'
|
|
142
|
+
|
|
143
|
+
const TEMPLATES = [
|
|
144
|
+
{
|
|
145
|
+
name: 'nextjs',
|
|
146
|
+
message:
|
|
147
|
+
'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'saas-starter',
|
|
151
|
+
message:
|
|
152
|
+
'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'api',
|
|
156
|
+
message: 'API server with Elysia, Drizzle, and TypeScript'
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
interface Options {
|
|
161
|
+
name?: string
|
|
162
|
+
description?: string
|
|
163
|
+
template?: string
|
|
164
|
+
git?: boolean
|
|
165
|
+
update?: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
program
|
|
169
|
+
.name('create-ej-app')
|
|
170
|
+
.description(
|
|
171
|
+
'Create a new app from a template. Flags answer the prompts; anything left out is asked.'
|
|
172
|
+
)
|
|
173
|
+
.option('-n, --name <name>', 'project name, also the folder name')
|
|
174
|
+
.option('-d, --description <text>', 'project description')
|
|
175
|
+
.option(
|
|
176
|
+
'-t, --template <template>',
|
|
177
|
+
`template: ${TEMPLATES.map(t => t.name).join(', ')}`
|
|
178
|
+
)
|
|
179
|
+
.option('--git', 'initialize a git repository')
|
|
180
|
+
.option('--no-git', 'skip git initialization')
|
|
181
|
+
.option(
|
|
182
|
+
'--update',
|
|
183
|
+
'update packages to their latest versions after creating the project'
|
|
184
|
+
)
|
|
185
|
+
.option('--no-update', 'keep the template package versions')
|
|
186
|
+
|
|
187
|
+
program.action(async (options: Options) => {
|
|
188
|
+
if (options.name !== undefined && !PROJECT_NAME_PATTERN.test(options.name)) {
|
|
189
|
+
console.error(`Invalid project name '${options.name}'. ${PROJECT_NAME_HINT}`)
|
|
190
|
+
process.exit(1)
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
options.template !== undefined &&
|
|
194
|
+
!TEMPLATES.some(t => t.name === options.template)
|
|
195
|
+
) {
|
|
196
|
+
console.error(
|
|
197
|
+
`Unknown template '${options.template}'. Use one of: ${TEMPLATES.map(t => t.name).join(', ')}.`
|
|
198
|
+
)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const questions = [
|
|
203
|
+
options.name === undefined && {
|
|
72
204
|
type: 'input',
|
|
73
205
|
name: 'projectName',
|
|
74
206
|
message: 'Enter the project name:',
|
|
75
207
|
initial: 'my-project',
|
|
76
|
-
// The name becomes a directory and the package.json name, so keep it
|
|
77
|
-
// to characters that are safe for both.
|
|
78
208
|
validate: (value: string) =>
|
|
79
|
-
|
|
80
|
-
'Use lowercase letters, numbers, dots, hyphens, and underscores.'
|
|
209
|
+
PROJECT_NAME_PATTERN.test(value) || PROJECT_NAME_HINT
|
|
81
210
|
},
|
|
82
|
-
{
|
|
211
|
+
options.description === undefined && {
|
|
83
212
|
type: 'input',
|
|
84
213
|
name: 'description',
|
|
85
214
|
message: 'Enter a description for the project:',
|
|
86
215
|
initial: 'A Legendary Project'
|
|
87
216
|
},
|
|
88
|
-
{
|
|
217
|
+
options.template === undefined && {
|
|
89
218
|
type: 'select',
|
|
90
219
|
name: 'template',
|
|
91
220
|
message: 'Select a template:',
|
|
92
|
-
choices:
|
|
93
|
-
{
|
|
94
|
-
name: 'nextjs',
|
|
95
|
-
message:
|
|
96
|
-
'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'saas-starter',
|
|
100
|
-
message:
|
|
101
|
-
'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'api',
|
|
105
|
-
message: 'API server with Elysia, Drizzle, and TypeScript'
|
|
106
|
-
}
|
|
107
|
-
]
|
|
221
|
+
choices: TEMPLATES
|
|
108
222
|
},
|
|
109
|
-
{
|
|
223
|
+
options.git === undefined && {
|
|
110
224
|
type: 'select',
|
|
111
225
|
name: 'git',
|
|
112
226
|
message: 'Initialize a git repository?',
|
|
113
227
|
choices: ['yes', 'no']
|
|
228
|
+
},
|
|
229
|
+
options.update === undefined && {
|
|
230
|
+
type: 'select',
|
|
231
|
+
name: 'update',
|
|
232
|
+
message:
|
|
233
|
+
'Update packages to their latest versions? The template can lag behind.',
|
|
234
|
+
choices: ['yes', 'no']
|
|
114
235
|
}
|
|
115
|
-
])
|
|
236
|
+
].filter(q => q !== false)
|
|
237
|
+
|
|
238
|
+
// Without a terminal the prompts would wait forever, for example when an
|
|
239
|
+
// agent or a script runs the CLI, so name the missing flags instead.
|
|
240
|
+
if (questions.length && !process.stdin.isTTY) {
|
|
241
|
+
const flags: Record<string, string> = {
|
|
242
|
+
projectName: '--name',
|
|
243
|
+
description: '--description',
|
|
244
|
+
template: '--template',
|
|
245
|
+
git: '--git or --no-git',
|
|
246
|
+
update: '--update or --no-update'
|
|
247
|
+
}
|
|
248
|
+
console.error(
|
|
249
|
+
`No terminal to ask questions in. Pass ${questions.map(q => flags[q.name]).join(', ')}.`
|
|
250
|
+
)
|
|
251
|
+
process.exit(1)
|
|
252
|
+
}
|
|
116
253
|
|
|
117
|
-
const
|
|
254
|
+
const answers: Partial<Response> = questions.length
|
|
255
|
+
? await enquirer.prompt(questions)
|
|
256
|
+
: {}
|
|
257
|
+
|
|
258
|
+
const projectName = options.name ?? answers.projectName!
|
|
259
|
+
const description = options.description ?? answers.description!
|
|
260
|
+
const template = options.template ?? answers.template!
|
|
261
|
+
const git =
|
|
262
|
+
options.git === undefined ? answers.git! : options.git ? 'yes' : 'no'
|
|
263
|
+
const update =
|
|
264
|
+
options.update === undefined
|
|
265
|
+
? answers.update!
|
|
266
|
+
: options.update
|
|
267
|
+
? 'yes'
|
|
268
|
+
: 'no'
|
|
118
269
|
|
|
119
270
|
const projectDir = path.join(process.cwd(), projectName)
|
|
120
271
|
|
|
@@ -186,19 +337,45 @@ program.action(async () => {
|
|
|
186
337
|
fs.writeFileSync(readmeFile, `# ${projectName}\n\n${description}\n`)
|
|
187
338
|
}
|
|
188
339
|
|
|
340
|
+
const pm = detectPackageManager()
|
|
341
|
+
const installCommand = getInstallCommand(pm)
|
|
342
|
+
const devCommand = getRunCommand(pm, 'dev')
|
|
343
|
+
const dbPushCommand = getRunCommand(pm, 'db:push')
|
|
344
|
+
|
|
345
|
+
// Updating also installs, so the initial commit includes the lockfile
|
|
346
|
+
let installed = false
|
|
347
|
+
if (update === 'yes') {
|
|
348
|
+
const commands = getUpdateCommands(pm)
|
|
349
|
+
if (!commands) {
|
|
350
|
+
console.log(
|
|
351
|
+
`\nAutomatic updates support Bun and pnpm. After '${installCommand}', update packages with your package manager.`
|
|
352
|
+
)
|
|
353
|
+
} else {
|
|
354
|
+
const command = process.stdin.isTTY ? commands.interactive : commands.all
|
|
355
|
+
console.log(`\nUpdating packages with '${command}'…`)
|
|
356
|
+
try {
|
|
357
|
+
execSync(command, { cwd: projectDir, stdio: 'inherit' })
|
|
358
|
+
installed = true
|
|
359
|
+
} catch {
|
|
360
|
+
console.error(
|
|
361
|
+
`Updating packages failed, so the template versions stay. Run '${command}' in the project to try again.`
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
189
367
|
if (git === 'yes') {
|
|
190
368
|
try {
|
|
191
|
-
|
|
369
|
+
execFileSync('git', ['init', '-b', defaultBranch()], {
|
|
370
|
+
cwd: projectDir,
|
|
371
|
+
stdio: 'inherit'
|
|
372
|
+
})
|
|
373
|
+
commitEverything(projectDir)
|
|
192
374
|
} catch (err) {
|
|
193
375
|
console.error('Error initializing git repository:', err)
|
|
194
376
|
}
|
|
195
377
|
}
|
|
196
378
|
|
|
197
|
-
const pm = detectPackageManager()
|
|
198
|
-
const installCommand = getInstallCommand(pm)
|
|
199
|
-
const devCommand = getRunCommand(pm, 'dev')
|
|
200
|
-
const dbPushCommand = getRunCommand(pm, 'db:push')
|
|
201
|
-
|
|
202
379
|
console.log(
|
|
203
380
|
`
|
|
204
381
|
Created a new app called '${projectName}' 🎉
|
|
@@ -210,9 +387,7 @@ code ${projectName}
|
|
|
210
387
|
|
|
211
388
|
To get started, run these commands:
|
|
212
389
|
cd ${projectName}
|
|
213
|
-
|
|
214
|
-
${installCommand}
|
|
215
|
-
|
|
390
|
+
${installed ? '' : `\n${installCommand}\n`}
|
|
216
391
|
Fill in the values in .env, then create the database tables:
|
|
217
392
|
|
|
218
393
|
${dbPushCommand}
|