create-ej-app 29.0.0 → 31.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.
Files changed (4) hide show
  1. package/README.md +21 -1
  2. package/cli.js +146 -33
  3. package/cli.ts +190 -36
  4. package/package.json +2 -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 also 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
@@ -48,54 +48,149 @@ 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
+ * Commits the new project once. Skips the commit when .env is not ignored, so
71
+ * the generated secret never lands in history, and explains a missing git
72
+ * identity instead of failing silently.
73
+ */
74
+ const commitEverything = (projectDir) => {
75
+ const run = (command) => execSync(command, { cwd: projectDir, stdio: 'pipe' });
76
+ try {
77
+ run('git check-ignore -q .env');
78
+ }
79
+ catch {
80
+ console.error('Skipped the initial commit: .env is not in .gitignore, and committing it would publish its secrets.');
81
+ return;
82
+ }
83
+ try {
84
+ run('git add -A');
85
+ run('git commit -q -m "initial commit: bootstrap new project with create-ej-app"');
86
+ console.log('Created the initial commit.');
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ console.error(/user\.(name|email)|identity/i.test(message)
91
+ ? "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'."
92
+ : `The initial commit failed: ${message}`);
93
+ }
94
+ };
51
95
  const getRunCommand = (packageManager, script) => packageManager === 'npm'
52
96
  ? `npm run ${script}`
53
97
  : `${packageManager} run ${script}`;
54
- program.action(async () => {
55
- const response = await enquirer.prompt([
56
- {
98
+ // The name becomes a directory and the package.json name, so keep it to
99
+ // characters that are safe for both.
100
+ const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/;
101
+ const PROJECT_NAME_HINT = 'Use lowercase letters, numbers, dots, hyphens, and underscores.';
102
+ const TEMPLATES = [
103
+ {
104
+ name: 'nextjs',
105
+ message: 'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
106
+ },
107
+ {
108
+ name: 'saas-starter',
109
+ message: 'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
110
+ },
111
+ {
112
+ name: 'api',
113
+ message: 'API server with Elysia, Drizzle, and TypeScript'
114
+ }
115
+ ];
116
+ program
117
+ .name('create-ej-app')
118
+ .description('Create a new app from a template. Flags answer the prompts; anything left out is asked.')
119
+ .option('-n, --name <name>', 'project name, also the folder name')
120
+ .option('-d, --description <text>', 'project description')
121
+ .option('-t, --template <template>', `template: ${TEMPLATES.map(t => t.name).join(', ')}`)
122
+ .option('--git', 'initialize a git repository')
123
+ .option('--no-git', 'skip git initialization')
124
+ .option('--update', 'update packages to their latest versions after creating the project')
125
+ .option('--no-update', 'keep the template package versions');
126
+ program.action(async (options) => {
127
+ if (options.name !== undefined && !PROJECT_NAME_PATTERN.test(options.name)) {
128
+ console.error(`Invalid project name '${options.name}'. ${PROJECT_NAME_HINT}`);
129
+ process.exit(1);
130
+ }
131
+ if (options.template !== undefined &&
132
+ !TEMPLATES.some(t => t.name === options.template)) {
133
+ console.error(`Unknown template '${options.template}'. Use one of: ${TEMPLATES.map(t => t.name).join(', ')}.`);
134
+ process.exit(1);
135
+ }
136
+ const questions = [
137
+ options.name === undefined && {
57
138
  type: 'input',
58
139
  name: 'projectName',
59
140
  message: 'Enter the project name:',
60
141
  initial: 'my-project',
61
- // The name becomes a directory and the package.json name, so keep it
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.'
142
+ validate: (value) => PROJECT_NAME_PATTERN.test(value) || PROJECT_NAME_HINT
65
143
  },
66
- {
144
+ options.description === undefined && {
67
145
  type: 'input',
68
146
  name: 'description',
69
147
  message: 'Enter a description for the project:',
70
148
  initial: 'A Legendary Project'
71
149
  },
72
- {
150
+ options.template === undefined && {
73
151
  type: 'select',
74
152
  name: 'template',
75
153
  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
- ]
154
+ choices: TEMPLATES
90
155
  },
91
- {
156
+ options.git === undefined && {
92
157
  type: 'select',
93
158
  name: 'git',
94
159
  message: 'Initialize a git repository?',
95
160
  choices: ['yes', 'no']
161
+ },
162
+ options.update === undefined && {
163
+ type: 'select',
164
+ name: 'update',
165
+ message: 'Update packages to their latest versions? The template can lag behind.',
166
+ choices: ['yes', 'no']
96
167
  }
97
- ]);
98
- const { projectName, description, template, git } = response;
168
+ ].filter(q => q !== false);
169
+ // Without a terminal the prompts would wait forever, for example when an
170
+ // agent or a script runs the CLI, so name the missing flags instead.
171
+ if (questions.length && !process.stdin.isTTY) {
172
+ const flags = {
173
+ projectName: '--name',
174
+ description: '--description',
175
+ template: '--template',
176
+ git: '--git or --no-git',
177
+ update: '--update or --no-update'
178
+ };
179
+ console.error(`No terminal to ask questions in. Pass ${questions.map(q => flags[q.name]).join(', ')}.`);
180
+ process.exit(1);
181
+ }
182
+ const answers = questions.length
183
+ ? await enquirer.prompt(questions)
184
+ : {};
185
+ const projectName = options.name ?? answers.projectName;
186
+ const description = options.description ?? answers.description;
187
+ const template = options.template ?? answers.template;
188
+ const git = options.git === undefined ? answers.git : options.git ? 'yes' : 'no';
189
+ const update = options.update === undefined
190
+ ? answers.update
191
+ : options.update
192
+ ? 'yes'
193
+ : 'no';
99
194
  const projectDir = path.join(process.cwd(), projectName);
100
195
  if (fs.existsSync(projectDir)) {
101
196
  console.error(`Directory '${projectName}' already exists. Choose another project name or remove the directory.`);
@@ -146,18 +241,38 @@ program.action(async () => {
146
241
  if (!fs.existsSync(readmeFile)) {
147
242
  fs.writeFileSync(readmeFile, `# ${projectName}\n\n${description}\n`);
148
243
  }
244
+ const pm = detectPackageManager();
245
+ const installCommand = getInstallCommand(pm);
246
+ const devCommand = getRunCommand(pm, 'dev');
247
+ const dbPushCommand = getRunCommand(pm, 'db:push');
248
+ // Updating also installs, so the initial commit includes the lockfile
249
+ let installed = false;
250
+ if (update === 'yes') {
251
+ const commands = getUpdateCommands(pm);
252
+ if (!commands) {
253
+ console.log(`\nAutomatic updates support Bun and pnpm. After '${installCommand}', update packages with your package manager.`);
254
+ }
255
+ else {
256
+ const command = process.stdin.isTTY ? commands.interactive : commands.all;
257
+ console.log(`\nUpdating packages with '${command}'…`);
258
+ try {
259
+ execSync(command, { cwd: projectDir, stdio: 'inherit' });
260
+ installed = true;
261
+ }
262
+ catch {
263
+ console.error(`Updating packages failed, so the template versions stay. Run '${command}' in the project to try again.`);
264
+ }
265
+ }
266
+ }
149
267
  if (git === 'yes') {
150
268
  try {
151
269
  execSync('git init', { cwd: projectDir, stdio: 'inherit' });
270
+ commitEverything(projectDir);
152
271
  }
153
272
  catch (err) {
154
273
  console.error('Error initializing git repository:', err);
155
274
  }
156
275
  }
157
- const pm = detectPackageManager();
158
- const installCommand = getInstallCommand(pm);
159
- const devCommand = getRunCommand(pm, 'dev');
160
- const dbPushCommand = getRunCommand(pm, 'db:push');
161
276
  console.log(`
162
277
  Created a new app called '${projectName}' 🎉
163
278
 
@@ -168,9 +283,7 @@ code ${projectName}
168
283
 
169
284
  To get started, run these commands:
170
285
  cd ${projectName}
171
-
172
- ${installCommand}
173
-
286
+ ${installed ? '' : `\n${installCommand}\n`}
174
287
  Fill in the values in .env, then create the database tables:
175
288
 
176
289
  ${dbPushCommand}
package/cli.ts CHANGED
@@ -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,192 @@ 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
+ * Commits the new project once. Skips the commit when .env is not ignored, so
86
+ * the generated secret never lands in history, and explains a missing git
87
+ * identity instead of failing silently.
88
+ */
89
+ const commitEverything = (projectDir: string) => {
90
+ const run = (command: string) =>
91
+ execSync(command, { cwd: projectDir, stdio: 'pipe' })
92
+ try {
93
+ run('git check-ignore -q .env')
94
+ } catch {
95
+ console.error(
96
+ 'Skipped the initial commit: .env is not in .gitignore, and committing it would publish its secrets.'
97
+ )
98
+ return
99
+ }
100
+ try {
101
+ run('git add -A')
102
+ run('git commit -q -m "initial commit: bootstrap new project with create-ej-app"')
103
+ console.log('Created the initial commit.')
104
+ } catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err)
106
+ console.error(
107
+ /user\.(name|email)|identity/i.test(message)
108
+ ? "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'."
109
+ : `The initial commit failed: ${message}`
110
+ )
111
+ }
112
+ }
113
+
64
114
  const getRunCommand = (packageManager: string, script: string) =>
65
115
  packageManager === 'npm'
66
116
  ? `npm run ${script}`
67
117
  : `${packageManager} run ${script}`
68
118
 
69
- program.action(async () => {
70
- const response: Response = await enquirer.prompt([
71
- {
119
+ // The name becomes a directory and the package.json name, so keep it to
120
+ // characters that are safe for both.
121
+ const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/
122
+ const PROJECT_NAME_HINT =
123
+ 'Use lowercase letters, numbers, dots, hyphens, and underscores.'
124
+
125
+ const TEMPLATES = [
126
+ {
127
+ name: 'nextjs',
128
+ message:
129
+ 'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
130
+ },
131
+ {
132
+ name: 'saas-starter',
133
+ message:
134
+ 'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
135
+ },
136
+ {
137
+ name: 'api',
138
+ message: 'API server with Elysia, Drizzle, and TypeScript'
139
+ }
140
+ ]
141
+
142
+ interface Options {
143
+ name?: string
144
+ description?: string
145
+ template?: string
146
+ git?: boolean
147
+ update?: boolean
148
+ }
149
+
150
+ program
151
+ .name('create-ej-app')
152
+ .description(
153
+ 'Create a new app from a template. Flags answer the prompts; anything left out is asked.'
154
+ )
155
+ .option('-n, --name <name>', 'project name, also the folder name')
156
+ .option('-d, --description <text>', 'project description')
157
+ .option(
158
+ '-t, --template <template>',
159
+ `template: ${TEMPLATES.map(t => t.name).join(', ')}`
160
+ )
161
+ .option('--git', 'initialize a git repository')
162
+ .option('--no-git', 'skip git initialization')
163
+ .option(
164
+ '--update',
165
+ 'update packages to their latest versions after creating the project'
166
+ )
167
+ .option('--no-update', 'keep the template package versions')
168
+
169
+ program.action(async (options: Options) => {
170
+ if (options.name !== undefined && !PROJECT_NAME_PATTERN.test(options.name)) {
171
+ console.error(`Invalid project name '${options.name}'. ${PROJECT_NAME_HINT}`)
172
+ process.exit(1)
173
+ }
174
+ if (
175
+ options.template !== undefined &&
176
+ !TEMPLATES.some(t => t.name === options.template)
177
+ ) {
178
+ console.error(
179
+ `Unknown template '${options.template}'. Use one of: ${TEMPLATES.map(t => t.name).join(', ')}.`
180
+ )
181
+ process.exit(1)
182
+ }
183
+
184
+ const questions = [
185
+ options.name === undefined && {
72
186
  type: 'input',
73
187
  name: 'projectName',
74
188
  message: 'Enter the project name:',
75
189
  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
190
  validate: (value: string) =>
79
- /^[a-z0-9][a-z0-9._-]{0,213}$/.test(value) ||
80
- 'Use lowercase letters, numbers, dots, hyphens, and underscores.'
191
+ PROJECT_NAME_PATTERN.test(value) || PROJECT_NAME_HINT
81
192
  },
82
- {
193
+ options.description === undefined && {
83
194
  type: 'input',
84
195
  name: 'description',
85
196
  message: 'Enter a description for the project:',
86
197
  initial: 'A Legendary Project'
87
198
  },
88
- {
199
+ options.template === undefined && {
89
200
  type: 'select',
90
201
  name: 'template',
91
202
  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
- ]
203
+ choices: TEMPLATES
108
204
  },
109
- {
205
+ options.git === undefined && {
110
206
  type: 'select',
111
207
  name: 'git',
112
208
  message: 'Initialize a git repository?',
113
209
  choices: ['yes', 'no']
210
+ },
211
+ options.update === undefined && {
212
+ type: 'select',
213
+ name: 'update',
214
+ message:
215
+ 'Update packages to their latest versions? The template can lag behind.',
216
+ choices: ['yes', 'no']
217
+ }
218
+ ].filter(q => q !== false)
219
+
220
+ // Without a terminal the prompts would wait forever, for example when an
221
+ // agent or a script runs the CLI, so name the missing flags instead.
222
+ if (questions.length && !process.stdin.isTTY) {
223
+ const flags: Record<string, string> = {
224
+ projectName: '--name',
225
+ description: '--description',
226
+ template: '--template',
227
+ git: '--git or --no-git',
228
+ update: '--update or --no-update'
114
229
  }
115
- ])
230
+ console.error(
231
+ `No terminal to ask questions in. Pass ${questions.map(q => flags[q.name]).join(', ')}.`
232
+ )
233
+ process.exit(1)
234
+ }
116
235
 
117
- const { projectName, description, template, git } = response
236
+ const answers: Partial<Response> = questions.length
237
+ ? await enquirer.prompt(questions)
238
+ : {}
239
+
240
+ const projectName = options.name ?? answers.projectName!
241
+ const description = options.description ?? answers.description!
242
+ const template = options.template ?? answers.template!
243
+ const git =
244
+ options.git === undefined ? answers.git! : options.git ? 'yes' : 'no'
245
+ const update =
246
+ options.update === undefined
247
+ ? answers.update!
248
+ : options.update
249
+ ? 'yes'
250
+ : 'no'
118
251
 
119
252
  const projectDir = path.join(process.cwd(), projectName)
120
253
 
@@ -186,19 +319,42 @@ program.action(async () => {
186
319
  fs.writeFileSync(readmeFile, `# ${projectName}\n\n${description}\n`)
187
320
  }
188
321
 
322
+ const pm = detectPackageManager()
323
+ const installCommand = getInstallCommand(pm)
324
+ const devCommand = getRunCommand(pm, 'dev')
325
+ const dbPushCommand = getRunCommand(pm, 'db:push')
326
+
327
+ // Updating also installs, so the initial commit includes the lockfile
328
+ let installed = false
329
+ if (update === 'yes') {
330
+ const commands = getUpdateCommands(pm)
331
+ if (!commands) {
332
+ console.log(
333
+ `\nAutomatic updates support Bun and pnpm. After '${installCommand}', update packages with your package manager.`
334
+ )
335
+ } else {
336
+ const command = process.stdin.isTTY ? commands.interactive : commands.all
337
+ console.log(`\nUpdating packages with '${command}'…`)
338
+ try {
339
+ execSync(command, { cwd: projectDir, stdio: 'inherit' })
340
+ installed = true
341
+ } catch {
342
+ console.error(
343
+ `Updating packages failed, so the template versions stay. Run '${command}' in the project to try again.`
344
+ )
345
+ }
346
+ }
347
+ }
348
+
189
349
  if (git === 'yes') {
190
350
  try {
191
351
  execSync('git init', { cwd: projectDir, stdio: 'inherit' })
352
+ commitEverything(projectDir)
192
353
  } catch (err) {
193
354
  console.error('Error initializing git repository:', err)
194
355
  }
195
356
  }
196
357
 
197
- const pm = detectPackageManager()
198
- const installCommand = getInstallCommand(pm)
199
- const devCommand = getRunCommand(pm, 'dev')
200
- const dbPushCommand = getRunCommand(pm, 'db:push')
201
-
202
358
  console.log(
203
359
  `
204
360
  Created a new app called '${projectName}' 🎉
@@ -210,9 +366,7 @@ code ${projectName}
210
366
 
211
367
  To get started, run these commands:
212
368
  cd ${projectName}
213
-
214
- ${installCommand}
215
-
369
+ ${installed ? '' : `\n${installCommand}\n`}
216
370
  Fill in the values in .env, then create the database tables:
217
371
 
218
372
  ${dbPushCommand}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "create-ej-app",
3
- "version": "29.0.0",
3
+ "version": "31.0.0",
4
4
  "description": "Bootstrap a next.js app with my preferred setup",
5
5
  "main": "cli.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
+ "prepublishOnly": "npm run build",
9
10
  "start": "node ."
10
11
  },
11
12
  "bin": {