@mvp-kit/create 0.0.10 → 0.0.11
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 +49 -16
- package/dist/create.js +754 -571
- package/dist/create.js.map +1 -1
- package/dist/download.js +62 -0
- package/dist/download.js.map +1 -0
- package/dist/index.js +24 -42
- package/dist/index.js.map +1 -1
- package/dist/template.js +59 -158
- package/dist/template.js.map +1 -1
- package/dist/utils.js +0 -88
- package/dist/utils.js.map +1 -1
- package/package.json +21 -24
- package/dist/database.js +0 -105
- package/dist/database.js.map +0 -1
- package/dist/template-downloader.js +0 -248
- package/dist/template-downloader.js.map +0 -1
- package/dist/test.js +0 -359
- package/dist/test.js.map +0 -1
package/dist/create.js
CHANGED
|
@@ -1,629 +1,812 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import {
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
29
|
-
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import prompts from 'prompts';
|
|
8
|
+
import { processTemplate, shouldSkipFile, getTargetFilePath } from './template.js';
|
|
9
|
+
import { downloadTemplate } from './download.js';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
/**
|
|
12
|
+
* Get path to template directory
|
|
13
|
+
* Supports multiple ways to specify template location:
|
|
14
|
+
* 1. --template-path CLI option (passed via options.templatePath)
|
|
15
|
+
* 2. MVPKIT_TEMPLATE_PATH environment variable
|
|
16
|
+
* 3. Download from GitHub releases using --template option
|
|
17
|
+
*/
|
|
18
|
+
async function getTemplatePath(options = {}) {
|
|
19
|
+
// 1. CLI option takes highest priority
|
|
20
|
+
if (options.templatePath) {
|
|
21
|
+
const templatePath = path.resolve(options.templatePath);
|
|
22
|
+
if (fs.existsSync(templatePath)) {
|
|
23
|
+
return templatePath;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Template path not found: ${templatePath}`);
|
|
26
|
+
}
|
|
27
|
+
// 2. Environment variable
|
|
28
|
+
if (process.env.MVPKIT_TEMPLATE_PATH) {
|
|
29
|
+
const templatePath = path.resolve(process.env.MVPKIT_TEMPLATE_PATH);
|
|
30
|
+
if (fs.existsSync(templatePath)) {
|
|
31
|
+
return templatePath;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Template path from MVPKIT_TEMPLATE_PATH not found: ${templatePath}`);
|
|
34
|
+
}
|
|
35
|
+
// 3. Download from GitHub based on template option
|
|
36
|
+
const template = options.template || 'core';
|
|
37
|
+
const cacheDir = path.join(os.tmpdir(), 'mvpkit-template-cache');
|
|
38
|
+
const templateDir = path.join(cacheDir, template);
|
|
39
|
+
// Check if caching is disabled or if we don't have a cached template
|
|
40
|
+
const useCache = options.cache !== false;
|
|
41
|
+
const hasCachedTemplate = useCache && fs.existsSync(templateDir) && fs.readdirSync(templateDir).length > 0;
|
|
42
|
+
if (hasCachedTemplate) {
|
|
43
|
+
return templateDir;
|
|
30
44
|
}
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
// Download the template (this will clear the directory if it exists)
|
|
46
|
+
return await downloadTemplate(template, templateDir);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read template configuration from template.json
|
|
50
|
+
*/
|
|
51
|
+
async function readTemplateConfig(templateDir) {
|
|
52
|
+
try {
|
|
53
|
+
const configPath = path.join(templateDir, 'template.json');
|
|
54
|
+
if (fs.existsSync(configPath)) {
|
|
55
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
56
|
+
return JSON.parse(configContent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.log(chalk.yellow('⚠️ Could not read template configuration'));
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
34
63
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Collect missing template variables interactively
|
|
66
|
+
*/
|
|
67
|
+
async function collectVariables(variables, options) {
|
|
68
|
+
// If non-interactive mode, use simple detection without prompts
|
|
69
|
+
if (options.interactive === false) {
|
|
70
|
+
let packageManager = 'pnpm';
|
|
71
|
+
let packageManagerVersion = '9.0.0';
|
|
72
|
+
// Simple detection based on user agent and argv - no prompts
|
|
73
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
74
|
+
const invocation = process.argv.join(' ');
|
|
75
|
+
if (userAgent.includes('bun/') || invocation.includes('bunx') || invocation.includes('bun create')) {
|
|
76
|
+
try {
|
|
77
|
+
packageManagerVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
78
|
+
packageManager = 'bun';
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fallback to pnpm if bun not available
|
|
82
|
+
packageManager = 'pnpm';
|
|
83
|
+
try {
|
|
84
|
+
packageManagerVersion = execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
packageManagerVersion = '9.0.0';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
45
90
|
}
|
|
46
91
|
else {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
92
|
+
// Default to pnpm for everything else (pnpm, npm, npx, unknown)
|
|
93
|
+
try {
|
|
94
|
+
packageManagerVersion = execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
packageManagerVersion = '9.0.0';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...variables,
|
|
102
|
+
packageManager,
|
|
103
|
+
packageManagerVersion,
|
|
104
|
+
componentPack: options.componentPack || 'basic',
|
|
105
|
+
git: options.git !== false
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const fieldsToCollect = [
|
|
109
|
+
// 1. Template selection (if not passed via CLI) - Currently only Core available
|
|
110
|
+
{
|
|
111
|
+
name: 'template',
|
|
112
|
+
message: 'Select template',
|
|
113
|
+
type: 'select',
|
|
114
|
+
choices: [
|
|
115
|
+
{ title: 'Core', value: 'core' }
|
|
116
|
+
],
|
|
117
|
+
initial: 0,
|
|
118
|
+
when: () => !options.template && !options.templatePath
|
|
119
|
+
},
|
|
120
|
+
// 2. Project description
|
|
121
|
+
{
|
|
122
|
+
name: 'projectDescription',
|
|
123
|
+
message: 'Project description',
|
|
124
|
+
type: 'text',
|
|
125
|
+
initial: options.description || `A full-stack application built with MVPKit`
|
|
126
|
+
},
|
|
127
|
+
// 3. Domain name
|
|
128
|
+
{
|
|
129
|
+
name: 'domainName',
|
|
130
|
+
message: 'Website domain (for deployment)',
|
|
131
|
+
type: 'text',
|
|
132
|
+
initial: options.domain || `${variables.projectName}.localhost`
|
|
133
|
+
},
|
|
134
|
+
// 4. Component pack selection
|
|
135
|
+
{
|
|
136
|
+
name: 'componentPack',
|
|
137
|
+
message: 'Choose component pack',
|
|
138
|
+
type: 'select',
|
|
139
|
+
choices: [
|
|
140
|
+
{ title: 'Essentials', value: 'basic' },
|
|
141
|
+
{ title: 'All', value: 'all' }
|
|
142
|
+
],
|
|
143
|
+
initial: 0,
|
|
144
|
+
when: () => !options.componentPack
|
|
145
|
+
},
|
|
146
|
+
// 5. Git initialization
|
|
147
|
+
{
|
|
148
|
+
name: 'git',
|
|
149
|
+
message: 'Initialize git repository?',
|
|
150
|
+
type: 'confirm',
|
|
151
|
+
initial: options.git !== false,
|
|
152
|
+
when: () => options.git === undefined
|
|
153
|
+
},
|
|
154
|
+
// 6. Package manager (last, after user sees what will be installed)
|
|
155
|
+
{
|
|
156
|
+
name: 'packageManager',
|
|
157
|
+
message: 'Choose package manager',
|
|
158
|
+
type: 'select',
|
|
159
|
+
choices: [
|
|
160
|
+
{ title: 'pnpm [stable]', value: 'pnpm' },
|
|
161
|
+
{ title: 'bun', value: 'bun' }
|
|
162
|
+
],
|
|
163
|
+
initial: 0
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
try {
|
|
167
|
+
// Check if we can use prompts (TTY available)
|
|
168
|
+
if (!process.stdin.isTTY) {
|
|
169
|
+
console.log(chalk.yellow('⚠️ Non-interactive environment detected, using defaults'));
|
|
170
|
+
const { packageManager, version: packageManagerVersion } = await detectPackageManager();
|
|
171
|
+
return {
|
|
172
|
+
...variables,
|
|
173
|
+
packageManager,
|
|
174
|
+
packageManagerVersion
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
console.log(chalk.cyan('🔧 Project Configuration'));
|
|
178
|
+
console.log(chalk.white.dim('═'.repeat(50)));
|
|
179
|
+
let responses;
|
|
180
|
+
try {
|
|
181
|
+
responses = await prompts(fieldsToCollect, {
|
|
182
|
+
onCancel: () => {
|
|
183
|
+
console.log(chalk.yellow('\n⚠️ Operation cancelled by user'));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (promptError) {
|
|
189
|
+
console.log(chalk.yellow('⚠️ Interactive prompts failed, using defaults'));
|
|
190
|
+
console.log(chalk.white.dim(` Error: ${promptError instanceof Error ? promptError.message : 'Unknown error'}`));
|
|
191
|
+
const { packageManager, version: packageManagerVersion } = await detectPackageManager();
|
|
192
|
+
return {
|
|
193
|
+
...variables,
|
|
194
|
+
packageManager,
|
|
195
|
+
packageManagerVersion
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Get package manager version
|
|
199
|
+
let packageManagerVersion = '9.0.0'; // default
|
|
200
|
+
if (responses.packageManager === 'bun') {
|
|
201
|
+
try {
|
|
202
|
+
packageManagerVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
packageManagerVersion = '1.0.0';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
try {
|
|
210
|
+
packageManagerVersion = execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
packageManagerVersion = '9.0.0';
|
|
214
|
+
}
|
|
50
215
|
}
|
|
216
|
+
return {
|
|
217
|
+
...variables,
|
|
218
|
+
template: responses.template || options.template || variables.template,
|
|
219
|
+
projectDescription: responses.projectDescription || variables.projectDescription,
|
|
220
|
+
domainName: responses.domainName || variables.domainName,
|
|
221
|
+
packageManager: responses.packageManager || 'pnpm',
|
|
222
|
+
packageManagerVersion,
|
|
223
|
+
componentPack: responses.componentPack || options.componentPack,
|
|
224
|
+
git: responses.git !== undefined ? responses.git : (options.git !== false)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.log(chalk.yellow('\n⚠️ Prompt collection failed, using defaults'));
|
|
229
|
+
const { packageManager, version: packageManagerVersion } = await detectPackageManager();
|
|
230
|
+
return {
|
|
231
|
+
...variables,
|
|
232
|
+
packageManager,
|
|
233
|
+
packageManagerVersion
|
|
234
|
+
};
|
|
51
235
|
}
|
|
52
|
-
if (currentLine)
|
|
53
|
-
lines.push(currentLine);
|
|
54
|
-
return lines;
|
|
55
236
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Complete project setup after template is copied
|
|
239
|
+
* Runs the same steps for both CLI and TUI modes
|
|
240
|
+
*/
|
|
241
|
+
async function completeProjectSetup(targetDir, variables, options) {
|
|
242
|
+
const packageManager = variables.packageManager;
|
|
243
|
+
// Step 1: Copy package manager specific files
|
|
244
|
+
await copyPackageManagerFiles(targetDir, variables, options);
|
|
245
|
+
// Step 2: Install dependencies
|
|
246
|
+
if (options.install !== false) {
|
|
247
|
+
await installDependencies(targetDir, packageManager);
|
|
248
|
+
}
|
|
249
|
+
// Step 3: Setup database
|
|
250
|
+
const apiPath = path.join(targetDir, 'apps', 'api');
|
|
251
|
+
if (fs.existsSync(apiPath) && options.install !== false) {
|
|
252
|
+
await setupDatabase(apiPath, packageManager);
|
|
253
|
+
}
|
|
254
|
+
// Step 4: Initialize shadcn in UI package
|
|
255
|
+
const uiPath = path.join(targetDir, 'packages', 'ui');
|
|
256
|
+
if (fs.existsSync(uiPath)) {
|
|
257
|
+
await initShadcnInUIPackage(uiPath, 'new-york', options.interactive !== false);
|
|
258
|
+
}
|
|
259
|
+
// Step 5: Install component pack
|
|
260
|
+
const componentPack = variables.componentPack;
|
|
261
|
+
if (componentPack && fs.existsSync(uiPath)) {
|
|
262
|
+
const templateConfig = await readTemplateConfig(targetDir);
|
|
263
|
+
await installComponentPack(uiPath, componentPack, options.interactive !== false, templateConfig);
|
|
264
|
+
}
|
|
265
|
+
// Step 6: Cleanup tasks
|
|
266
|
+
await cleanup(targetDir, packageManager);
|
|
267
|
+
// Step 7: Initialize git at the end
|
|
268
|
+
const shouldInitGit = variables.git;
|
|
269
|
+
if (shouldInitGit !== false) {
|
|
270
|
+
await initializeGitWithCommit(targetDir);
|
|
271
|
+
}
|
|
61
272
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
// Check for lock files in priority order
|
|
73
|
-
if (fs.existsSync(path.join(dir, 'bun.lockb'))) {
|
|
74
|
-
return { pm: 'bun', version: getPackageVersion('bun') };
|
|
75
|
-
}
|
|
76
|
-
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) {
|
|
77
|
-
return { pm: 'pnpm', version: getPackageVersion('pnpm') };
|
|
78
|
-
}
|
|
79
|
-
// Default to pnpm
|
|
80
|
-
return { pm: 'pnpm', version: '10.14.0' };
|
|
273
|
+
async function cleanup(targetDir, packageManager) {
|
|
274
|
+
// Only inherit theme name from UI to web
|
|
275
|
+
const uiPath = path.join(targetDir, 'packages', 'ui');
|
|
276
|
+
const webPath = path.join(targetDir, 'apps', 'web');
|
|
277
|
+
if (fs.existsSync(uiPath) && fs.existsSync(webPath)) {
|
|
278
|
+
await inheritUIConfigToWeb(uiPath, webPath);
|
|
279
|
+
}
|
|
280
|
+
// Run lint fix
|
|
281
|
+
await runLintFixCommand(targetDir, packageManager);
|
|
81
282
|
}
|
|
82
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Detect package manager based on how CLI was invoked
|
|
285
|
+
* - npx: Ask user to choose
|
|
286
|
+
* - pnpm create/pnpm dlx: Use pnpm
|
|
287
|
+
* - bunx: Use bun
|
|
288
|
+
*/
|
|
289
|
+
async function detectPackageManager() {
|
|
290
|
+
// Check how the CLI was invoked using environment variables and process.argv
|
|
291
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
292
|
+
const invocation = process.argv.join(' ');
|
|
293
|
+
// Check user agent for package manager detection
|
|
294
|
+
if (userAgent.includes('bun/')) {
|
|
295
|
+
try {
|
|
296
|
+
const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
297
|
+
return { packageManager: 'bun', version: bunVersion };
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
console.log(chalk.yellow('⚠️ Bun not available, falling back to pnpm'));
|
|
301
|
+
const pnpmVersion = await getPnpmVersion();
|
|
302
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (userAgent.includes('pnpm/')) {
|
|
306
|
+
const pnpmVersion = await getPnpmVersion();
|
|
307
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
308
|
+
}
|
|
309
|
+
// Check argv for direct invocations (fallback)
|
|
310
|
+
if (invocation.includes('bunx') || invocation.includes('bun create')) {
|
|
311
|
+
try {
|
|
312
|
+
const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
313
|
+
return { packageManager: 'bun', version: bunVersion };
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
console.log(chalk.yellow('⚠️ Bun not available, falling back to pnpm'));
|
|
317
|
+
const pnpmVersion = await getPnpmVersion();
|
|
318
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (invocation.includes('pnpm create') || invocation.includes('pnpm dlx')) {
|
|
322
|
+
const pnpmVersion = await getPnpmVersion();
|
|
323
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
324
|
+
}
|
|
325
|
+
// For npx (npm user agent) or unknown, ask user to choose
|
|
326
|
+
if (userAgent.includes('npm/') || invocation.includes('npx')) {
|
|
327
|
+
return await promptForPackageManager();
|
|
328
|
+
}
|
|
329
|
+
// Fallback: try to detect what's available
|
|
83
330
|
try {
|
|
84
|
-
const
|
|
85
|
-
return
|
|
331
|
+
const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
332
|
+
return { packageManager: 'bun', version: bunVersion };
|
|
86
333
|
}
|
|
87
334
|
catch {
|
|
88
|
-
|
|
335
|
+
const pnpmVersion = await getPnpmVersion();
|
|
336
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
89
337
|
}
|
|
90
338
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const commandLine = argv.join(' ');
|
|
102
|
-
if (commandLine.includes('bunx') || commandLine.includes('bun run')) {
|
|
103
|
-
return { pm: 'bun', version: extractVersion(userAgent) || 'latest' };
|
|
104
|
-
}
|
|
105
|
-
if (commandLine.includes('pnpm') || commandLine.includes('pnpx')) {
|
|
106
|
-
return { pm: 'pnpm', version: extractVersion(userAgent) || '10.14.0' };
|
|
107
|
-
}
|
|
108
|
-
if (commandLine.includes('npx') || commandLine.includes('npm create')) {
|
|
109
|
-
return { pm: 'pnpm', version: '10.14.0' };
|
|
110
|
-
}
|
|
111
|
-
// Check user agent string
|
|
112
|
-
if (userAgent.startsWith('pnpm'))
|
|
113
|
-
return { pm: 'pnpm', version: extractVersion(userAgent) || '10.14.0' };
|
|
114
|
-
if (userAgent.startsWith('bun'))
|
|
115
|
-
return { pm: 'bun', version: extractVersion(userAgent) || 'latest' };
|
|
116
|
-
if (userAgent.startsWith('npm'))
|
|
117
|
-
return { pm: 'pnpm', version: '10.14.0' };
|
|
118
|
-
return null;
|
|
339
|
+
/**
|
|
340
|
+
* Get pnpm version with fallback
|
|
341
|
+
*/
|
|
342
|
+
async function getPnpmVersion() {
|
|
343
|
+
try {
|
|
344
|
+
return execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return '9.0.0'; // Default version
|
|
348
|
+
}
|
|
119
349
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Prompt user to choose package manager (for npx invocation)
|
|
352
|
+
*/
|
|
353
|
+
async function promptForPackageManager() {
|
|
354
|
+
const inquirer = await import('inquirer');
|
|
355
|
+
// Check what's available
|
|
356
|
+
const bunAvailable = await checkCommandAvailable('bun');
|
|
357
|
+
const pnpmAvailable = await checkCommandAvailable('pnpm');
|
|
358
|
+
if (!bunAvailable && !pnpmAvailable) {
|
|
359
|
+
console.log(chalk.yellow('⚠️ Neither bun nor pnpm found, defaulting to pnpm'));
|
|
360
|
+
return { packageManager: 'pnpm', version: '9.0.0' };
|
|
361
|
+
}
|
|
362
|
+
const choices = [];
|
|
363
|
+
if (pnpmAvailable)
|
|
364
|
+
choices.push({ name: 'pnpm (recommended for most projects)', value: 'pnpm' });
|
|
365
|
+
if (bunAvailable)
|
|
366
|
+
choices.push({ name: 'bun (fast and modern)', value: 'bun' });
|
|
367
|
+
const { packageManager } = await inquirer.default.prompt([{
|
|
368
|
+
type: 'list',
|
|
369
|
+
name: 'packageManager',
|
|
370
|
+
message: 'Which package manager would you like to use?',
|
|
371
|
+
choices,
|
|
372
|
+
default: 'pnpm'
|
|
373
|
+
}]);
|
|
374
|
+
if (packageManager === 'bun') {
|
|
375
|
+
const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
376
|
+
return { packageManager: 'bun', version: bunVersion };
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const pnpmVersion = await getPnpmVersion();
|
|
380
|
+
return { packageManager: 'pnpm', version: pnpmVersion };
|
|
381
|
+
}
|
|
125
382
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Check if a command is available
|
|
385
|
+
*/
|
|
386
|
+
async function checkCommandAvailable(command) {
|
|
387
|
+
try {
|
|
388
|
+
execSync(`${command} --version`, { stdio: 'pipe' });
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
133
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Create project from template
|
|
397
|
+
*/
|
|
134
398
|
export async function createProject(projectName, options = {}) {
|
|
135
|
-
const targetDir = path.
|
|
136
|
-
//
|
|
137
|
-
console.log();
|
|
138
|
-
const logoWidth = BOX_WIDTH - 2; // Account for box borders and padding
|
|
139
|
-
const logoIndent = ' ';
|
|
140
|
-
console.log(chalk.cyan.bold(logoIndent + '╔' + '═'.repeat(logoWidth) + '╗'));
|
|
141
|
-
console.log(chalk.cyan.bold(logoIndent + '║') + chalk.bold.white(' '.repeat(Math.floor((logoWidth - 6) / 2)) + 'MVPKit' + ' '.repeat(Math.ceil((logoWidth - 6) / 2))) + chalk.cyan.bold('║'));
|
|
142
|
-
console.log(chalk.cyan.bold(logoIndent + '║') + chalk.gray(' '.repeat(Math.floor((logoWidth - 35) / 2)) + 'Cloudflare-native React Starter Kit' + ' '.repeat(Math.ceil((logoWidth - 35) / 2))) + chalk.cyan.bold('║'));
|
|
143
|
-
console.log(chalk.cyan.bold(logoIndent + '╚' + '═'.repeat(logoWidth) + '╝'));
|
|
144
|
-
console.log();
|
|
145
|
-
console.log(chalk.blue.bold(' 🚀 Ship production apps in minutes [DEV]'));
|
|
146
|
-
console.log();
|
|
147
|
-
// Validate project name and directory
|
|
399
|
+
const targetDir = path.join(process.cwd(), projectName);
|
|
400
|
+
// Validate target directory
|
|
148
401
|
if (fs.existsSync(targetDir)) {
|
|
149
402
|
throw new Error(`Directory ${projectName} already exists`);
|
|
150
403
|
}
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`Features: ${template.features.slice(0, 2).join(' • ')}`
|
|
165
|
-
], 'Template Selected'));
|
|
166
|
-
console.log();
|
|
167
|
-
// Generate template variables
|
|
168
|
-
let variables = generateTemplateVariables(projectName, options, packageManagerInfo.pm, packageManagerInfo.version);
|
|
169
|
-
// Interactive mode for project customization (default unless explicitly disabled)
|
|
170
|
-
if (options.interactive !== false) {
|
|
171
|
-
const { updatedVariables, updatedOptions } = await runInteractivePrompts(projectName, variables, options);
|
|
172
|
-
variables = updatedVariables;
|
|
173
|
-
Object.assign(options, updatedOptions);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Set opinionated defaults for non-interactive mode
|
|
177
|
-
options.install = true; // Always install dependencies
|
|
178
|
-
options.git = options.git !== false; // Create git repo unless explicitly disabled
|
|
179
|
-
options.setupDb = true; // Always setup database locally
|
|
180
|
-
options.setupShadcn = true; // Setup shadcn by default
|
|
181
|
-
}
|
|
182
|
-
// Project creation message
|
|
183
|
-
console.log(drawBox([
|
|
184
|
-
`🎯 Project: ${variables.projectDisplayName}`,
|
|
185
|
-
`📁 Location: ${targetDir}`,
|
|
186
|
-
`📋 Template: ${template.displayName}`,
|
|
187
|
-
'',
|
|
188
|
-
'🚀 Steps: Download → Install → Setup → Finalize'
|
|
189
|
-
], 'Creating Your Project'));
|
|
190
|
-
// Step 1: Download and copy template
|
|
191
|
-
const downloader = new TemplateDownloader();
|
|
192
|
-
const templatePath = await downloader.downloadTemplate(templateName, undefined, options.dev);
|
|
193
|
-
const copySpinner = ora('Copying template files...').start();
|
|
404
|
+
// Build initial template variables
|
|
405
|
+
const variables = {
|
|
406
|
+
template: options.template || 'core',
|
|
407
|
+
projectName,
|
|
408
|
+
projectDescription: options.description || `A full-stack application built with MVPKit`,
|
|
409
|
+
domainName: options.domain || `${projectName}.localhost`,
|
|
410
|
+
packageManager: 'pnpm', // Default, will be updated by prompts if needed
|
|
411
|
+
packageManagerVersion: '9.0.0' // Default, will be updated by prompts if needed
|
|
412
|
+
};
|
|
413
|
+
// CLI mode with interactive prompts
|
|
414
|
+
// Collect missing variables interactively
|
|
415
|
+
const finalVariables = await collectVariables(variables, options);
|
|
416
|
+
console.log(chalk.blue(`🚀 Creating ${projectName} with MVPKit Core\n`));
|
|
194
417
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
418
|
+
// Copy template and complete setup using shared functionality
|
|
419
|
+
await copyTemplate(targetDir, finalVariables, options);
|
|
420
|
+
await completeProjectSetup(targetDir, finalVariables, options);
|
|
421
|
+
// Success!
|
|
422
|
+
console.log('\n' + chalk.green('🎉 Project Created Successfully!'));
|
|
423
|
+
console.log(chalk.white.dim('═'.repeat(50)));
|
|
424
|
+
console.log(chalk.green(`✅ ${projectName} is ready to go!`));
|
|
425
|
+
// Next steps
|
|
426
|
+
const extendedOptions = { ...options, componentPack: finalVariables.componentPack };
|
|
427
|
+
printNextSteps(projectName, extendedOptions, finalVariables.packageManager);
|
|
197
428
|
}
|
|
198
429
|
catch (error) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
throw new Error(`Template copy failed: ${errorMessage}`);
|
|
203
|
-
}
|
|
204
|
-
// Update package manager for target directory
|
|
205
|
-
const targetPackageManagerInfo = detectPackageManager(targetDir);
|
|
206
|
-
const targetPmCommand = getPackageManagerCommand(targetPackageManagerInfo);
|
|
207
|
-
// Step 2: Install dependencies (always install unless explicitly disabled)
|
|
208
|
-
if (options.install !== false) {
|
|
209
|
-
const installSpinner = ora('Installing dependencies...').start();
|
|
210
|
-
try {
|
|
211
|
-
execSync(`${targetPmCommand} install`, { cwd: targetDir, stdio: 'ignore' });
|
|
212
|
-
installSpinner.succeed('Dependencies installed');
|
|
430
|
+
// Clean up on failure
|
|
431
|
+
if (fs.existsSync(targetDir)) {
|
|
432
|
+
await fs.remove(targetDir);
|
|
213
433
|
}
|
|
214
|
-
|
|
215
|
-
installSpinner.fail('Failed to install dependencies');
|
|
216
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
217
|
-
console.log(chalk.yellow(`💡 You can install them manually with: ${targetPmCommand} install`));
|
|
218
|
-
console.log(chalk.gray(` Error: ${errorMessage}`));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// Step 3: Setup database
|
|
222
|
-
let dbSetupCompleted = false;
|
|
223
|
-
if (options.setupDb !== false) {
|
|
224
|
-
dbSetupCompleted = await setupDatabase(targetDir, variables);
|
|
434
|
+
throw error;
|
|
225
435
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Initialize shadcn/ui ONLY in packages/ui using the official shadcn CLI
|
|
439
|
+
*/
|
|
440
|
+
async function initShadcnInUIPackage(uiPath, style, interactive) {
|
|
441
|
+
const viteConfigPath = path.join(uiPath, 'vite.config.ts');
|
|
442
|
+
try {
|
|
443
|
+
// Create temporary vite.config.ts for framework detection
|
|
444
|
+
const viteConfig = `import { defineConfig } from 'vite'
|
|
445
|
+
import react from '@vitejs/plugin-react'
|
|
446
|
+
|
|
447
|
+
export default defineConfig({
|
|
448
|
+
plugins: [react()],
|
|
449
|
+
})
|
|
450
|
+
`;
|
|
451
|
+
fs.writeFileSync(viteConfigPath, viteConfig);
|
|
452
|
+
if (interactive) {
|
|
453
|
+
// In interactive mode, let users choose through prompts, force override existing config
|
|
454
|
+
execSync('npx shadcn@latest init -f -s', {
|
|
455
|
+
cwd: uiPath,
|
|
456
|
+
stdio: 'inherit',
|
|
457
|
+
timeout: 60000 // 60 second timeout for CLI init
|
|
458
|
+
});
|
|
234
459
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
460
|
+
else {
|
|
461
|
+
// In non-interactive mode, use specific defaults (new-york, zinc) with -y flag and force override
|
|
462
|
+
execSync('npx shadcn@latest init -y -f -s -b zinc', {
|
|
463
|
+
cwd: uiPath,
|
|
464
|
+
stdio: 'pipe',
|
|
465
|
+
timeout: 60000 // 60 second timeout for CLI init
|
|
466
|
+
});
|
|
241
467
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
throw new Error(`Failed to initialize shadcn/ui in UI package: ${error instanceof Error ? error.message : String(error)}`);
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
// Clean up temporary vite.config.ts
|
|
474
|
+
if (fs.existsSync(viteConfigPath)) {
|
|
475
|
+
fs.unlinkSync(viteConfigPath);
|
|
246
476
|
}
|
|
247
|
-
|
|
248
|
-
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Install a component pack in the UI package
|
|
481
|
+
*/
|
|
482
|
+
async function installComponentPack(uiPath, packName, interactive, templateConfig) {
|
|
483
|
+
const spinner = ora(` Adding ${packName} component library...`).start();
|
|
484
|
+
try {
|
|
485
|
+
if (packName === 'all') {
|
|
486
|
+
// Use shadcn --all flag for bulk installation
|
|
487
|
+
const addArgs = [
|
|
488
|
+
'add',
|
|
489
|
+
interactive ? '' : '-y',
|
|
490
|
+
'--overwrite',
|
|
491
|
+
'--all'
|
|
492
|
+
].filter(Boolean);
|
|
493
|
+
execSync(`npx shadcn@latest ${addArgs.join(' ')}`, {
|
|
494
|
+
cwd: uiPath,
|
|
495
|
+
stdio: 'pipe',
|
|
496
|
+
timeout: 120000 // 2 minute timeout for bulk install
|
|
497
|
+
});
|
|
249
498
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
499
|
+
else {
|
|
500
|
+
// Use template config if available, otherwise use default component packs
|
|
501
|
+
const componentPacks = templateConfig?.componentPacks || {
|
|
502
|
+
basic: { components: ['button', 'card', 'input', 'label', 'separator'] }
|
|
503
|
+
};
|
|
504
|
+
if (!(packName in componentPacks)) {
|
|
505
|
+
throw new Error(`Unknown component pack: ${packName}. Available packs: ${Object.keys(componentPacks).join(', ')}`);
|
|
256
506
|
}
|
|
257
|
-
|
|
258
|
-
|
|
507
|
+
const components = componentPacks[packName].components;
|
|
508
|
+
// Install components individually
|
|
509
|
+
for (const component of components) {
|
|
510
|
+
const addArgs = [
|
|
511
|
+
'add',
|
|
512
|
+
interactive ? '' : '-y',
|
|
513
|
+
'--overwrite',
|
|
514
|
+
component
|
|
515
|
+
].filter(Boolean);
|
|
516
|
+
execSync(`npx shadcn@latest ${addArgs.join(' ')}`, {
|
|
517
|
+
cwd: uiPath,
|
|
518
|
+
stdio: 'pipe',
|
|
519
|
+
timeout: 30000 // 30 second timeout per component
|
|
520
|
+
});
|
|
259
521
|
}
|
|
260
522
|
}
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
'',
|
|
268
|
-
'🎨 Your Cloudflare-native MVP awaits!',
|
|
269
|
-
'⚡ Powered by MVPKit'
|
|
270
|
-
], 'Project Complete!'));
|
|
271
|
-
console.log();
|
|
272
|
-
printNextSteps(projectName, options, dbSetupCompleted, targetPackageManagerInfo.pm);
|
|
523
|
+
spinner.succeed(` ✅ ${packName} component library ready`);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
spinner.fail(` ❌ ${packName} component library installation failed`);
|
|
527
|
+
throw new Error(`Failed to install ${packName} component library: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
273
529
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{
|
|
306
|
-
type: 'confirm',
|
|
307
|
-
name: 'git',
|
|
308
|
-
message: 'Create git repository?',
|
|
309
|
-
default: true
|
|
310
|
-
},
|
|
311
|
-
]);
|
|
312
|
-
console.log(); // Add spacing after prompts
|
|
313
|
-
const updatedVariables = {
|
|
314
|
-
...variables,
|
|
315
|
-
projectDescription: answers.description,
|
|
316
|
-
domainName: answers.domain || undefined,
|
|
317
|
-
packageScope: answers.packageScope,
|
|
318
|
-
frontendPackageName: `${answers.packageScope}/frontend`,
|
|
319
|
-
backendPackageName: `${answers.packageScope}/backend`,
|
|
320
|
-
apiPackageName: `${answers.packageScope}/api`,
|
|
321
|
-
configPackageName: `${answers.packageScope}/config`
|
|
322
|
-
};
|
|
323
|
-
// Ensure projectKebabCase is never empty or undefined
|
|
324
|
-
if (!updatedVariables.projectKebabCase) {
|
|
325
|
-
console.warn('Warning: projectKebabCase is empty, using projectName as fallback');
|
|
326
|
-
updatedVariables.projectKebabCase = projectName.toLowerCase().replace(/[_\s]+/g, '-');
|
|
327
|
-
}
|
|
328
|
-
const updatedOptions = {
|
|
329
|
-
...options,
|
|
330
|
-
install: true, // Always install dependencies
|
|
331
|
-
git: answers.git,
|
|
332
|
-
setupDb: true, // Always setup database locally
|
|
333
|
-
setupShadcn: true // Always setup shadcn
|
|
334
|
-
};
|
|
335
|
-
return { updatedVariables, updatedOptions };
|
|
530
|
+
/**
|
|
531
|
+
* Inherit UI package configuration to web app components.json
|
|
532
|
+
* Web app gets config but NOT styles.css (styles come from UI package)
|
|
533
|
+
*/
|
|
534
|
+
async function inheritUIConfigToWeb(uiPath, webPath) {
|
|
535
|
+
const uiComponentsPath = path.join(uiPath, 'components.json');
|
|
536
|
+
const webComponentsPath = path.join(webPath, 'components.json');
|
|
537
|
+
if (!fs.existsSync(uiComponentsPath)) {
|
|
538
|
+
throw new Error('UI package components.json not found - cannot inherit configuration');
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const uiConfig = JSON.parse(fs.readFileSync(uiComponentsPath, 'utf-8'));
|
|
542
|
+
// Create web-specific config inheriting from UI
|
|
543
|
+
const webConfig = {
|
|
544
|
+
...uiConfig,
|
|
545
|
+
// Web-specific overrides
|
|
546
|
+
tailwind: {
|
|
547
|
+
...uiConfig.tailwind,
|
|
548
|
+
css: 'src/styles/globals.css' // Web app uses globals.css
|
|
549
|
+
},
|
|
550
|
+
aliases: {
|
|
551
|
+
...uiConfig.aliases,
|
|
552
|
+
ui: '@/components/ui' // Web app has different ui path
|
|
553
|
+
},
|
|
554
|
+
registries: {} // Web app gets fresh registries
|
|
555
|
+
};
|
|
556
|
+
await fs.writeJson(webComponentsPath, webConfig, { spaces: 2 });
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
throw new Error(`Failed to inherit UI config to web: ${error instanceof Error ? error.message : String(error)}`);
|
|
560
|
+
}
|
|
336
561
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
// For template files, remove template extension and transform filename
|
|
368
|
-
targetRelativePath = transformTemplateFileName(relativePath, variables);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
// For regular files, just transform filename
|
|
372
|
-
targetRelativePath = transformFileName(relativePath, variables);
|
|
373
|
-
}
|
|
374
|
-
const targetFile = path.join(targetDir, targetRelativePath);
|
|
375
|
-
// Ensure target directory exists
|
|
376
|
-
await fs.ensureDir(path.dirname(targetFile));
|
|
377
|
-
try {
|
|
378
|
-
// Read source file
|
|
379
|
-
const content = await fs.readFile(sourcePath, 'utf8');
|
|
380
|
-
// Get appropriate transformation function
|
|
381
|
-
const fileName = path.basename(sourceFile);
|
|
382
|
-
const transformFunction = getFileTransformFunction(fileName, isTemplateFile);
|
|
383
|
-
// Transform content
|
|
384
|
-
const transformedContent = transformFunction(content, variables);
|
|
385
|
-
// Special handling for package.json - remove workspaces to prevent nested workspace issues
|
|
386
|
-
if (targetRelativePath.endsWith('package.json') && !targetRelativePath.includes('node_modules')) {
|
|
387
|
-
try {
|
|
388
|
-
const pkg = JSON.parse(transformedContent);
|
|
389
|
-
// Remove workspaces from generated projects to prevent nested workspace conflicts
|
|
390
|
-
if (pkg.workspaces) {
|
|
391
|
-
delete pkg.workspaces;
|
|
392
|
-
}
|
|
393
|
-
// Also remove pnpm overrides if they exist
|
|
394
|
-
if (pkg.pnpm && pkg.pnpm.overrides) {
|
|
395
|
-
delete pkg.pnpm.overrides;
|
|
396
|
-
}
|
|
397
|
-
const finalContent = JSON.stringify(pkg, null, 2);
|
|
398
|
-
await fs.writeFile(targetFile, finalContent);
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
// If JSON parsing fails, write as-is
|
|
402
|
-
await fs.writeFile(targetFile, transformedContent);
|
|
403
|
-
}
|
|
562
|
+
/**
|
|
563
|
+
* Copy template files and process .template files
|
|
564
|
+
*/
|
|
565
|
+
export async function copyTemplate(targetDir, variables, options = {}) {
|
|
566
|
+
const spinner = ora('Setting up project structure...').start();
|
|
567
|
+
try {
|
|
568
|
+
const coreDir = await getTemplatePath(options);
|
|
569
|
+
// Get all files from core directory
|
|
570
|
+
const files = await glob('**/*', {
|
|
571
|
+
cwd: coreDir,
|
|
572
|
+
dot: true,
|
|
573
|
+
nodir: true,
|
|
574
|
+
absolute: false
|
|
575
|
+
});
|
|
576
|
+
// Process each file
|
|
577
|
+
for (const sourceFile of files) {
|
|
578
|
+
const sourcePath = path.join(coreDir, sourceFile);
|
|
579
|
+
// Skip unwanted files based on .templateignore
|
|
580
|
+
if (shouldSkipFile(sourceFile, coreDir)) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
// Determine target path
|
|
584
|
+
const targetFile = path.join(targetDir, getTargetFilePath(sourceFile));
|
|
585
|
+
// Ensure target directory exists
|
|
586
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
587
|
+
// Process template files
|
|
588
|
+
if (sourceFile.endsWith('.template')) {
|
|
589
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
590
|
+
const processedContent = processTemplate(content, variables);
|
|
591
|
+
await fs.writeFile(targetFile, processedContent);
|
|
404
592
|
}
|
|
405
593
|
else {
|
|
406
|
-
//
|
|
407
|
-
await fs.
|
|
594
|
+
// Copy regular files as-is
|
|
595
|
+
await fs.copy(sourcePath, targetFile);
|
|
408
596
|
}
|
|
409
597
|
}
|
|
410
|
-
|
|
411
|
-
// If file is binary or can't be read as text, copy as-is
|
|
412
|
-
await fs.copy(sourcePath, targetFile);
|
|
413
|
-
}
|
|
598
|
+
spinner.succeed('Project structure created');
|
|
414
599
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
600
|
+
catch (error) {
|
|
601
|
+
spinner.fail('Project structure creation failed');
|
|
602
|
+
throw error;
|
|
418
603
|
}
|
|
419
604
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
${variables.domainName ? `VITE_APP_DOMAIN=${variables.domainName}` : '# VITE_APP_DOMAIN=your-domain.com'}
|
|
426
|
-
`;
|
|
427
|
-
const backendDevVarsExample = `# Backend Development Variables
|
|
428
|
-
# Database
|
|
429
|
-
DATABASE_URL="file:./dev.db"
|
|
430
|
-
|
|
431
|
-
# Auth (generate your own secrets)
|
|
432
|
-
# AUTH_SECRET="your-secret-key-here"
|
|
433
|
-
# GITHUB_CLIENT_ID="your-github-client-id"
|
|
434
|
-
# GITHUB_CLIENT_SECRET="your-github-client-secret"
|
|
435
|
-
|
|
436
|
-
# Email (optional)
|
|
437
|
-
# SMTP_HOST="smtp.gmail.com"
|
|
438
|
-
# SMTP_PORT="587"
|
|
439
|
-
# SMTP_USER="your-email@gmail.com"
|
|
440
|
-
# SMTP_PASSWORD="your-password"
|
|
441
|
-
`;
|
|
442
|
-
await fs.writeFile(path.join(targetDir, 'apps/frontend/.env.example'), frontendEnvExample);
|
|
443
|
-
await fs.writeFile(path.join(targetDir, 'apps/backend/.dev.vars.example'), backendDevVarsExample);
|
|
444
|
-
// Create a project-specific README
|
|
445
|
-
const readmeContent = `# ${variables.projectDisplayName}
|
|
446
|
-
|
|
447
|
-
${variables.projectDescription}
|
|
448
|
-
|
|
449
|
-
Built with [MVPKit Core](https://mvpkit.dev) - A production-ready Cloudflare-native starter.
|
|
450
|
-
|
|
451
|
-
## Quick Start
|
|
452
|
-
|
|
453
|
-
\`\`\`bash
|
|
454
|
-
# Install dependencies
|
|
455
|
-
${getPackageManagerCommand(packageManagerInfo)} install
|
|
456
|
-
|
|
457
|
-
# Set up environment variables
|
|
458
|
-
cp apps/frontend/.env.example apps/frontend/.env.local
|
|
459
|
-
cp apps/backend/.dev.vars.example apps/backend/.dev.vars
|
|
460
|
-
|
|
461
|
-
# Set up database (if not done during project creation)
|
|
462
|
-
# cd apps/backend
|
|
463
|
-
# ${getPackageManagerCommand(packageManagerInfo)} db:migrate:local
|
|
464
|
-
# ${getPackageManagerCommand(packageManagerInfo)} db:seed:local
|
|
465
|
-
|
|
466
|
-
# Start development servers
|
|
467
|
-
${getPackageManagerCommand(packageManagerInfo)} dev
|
|
468
|
-
\`\`\`
|
|
469
|
-
|
|
470
|
-
Visit [http://localhost:5173](http://localhost:5173) to see your application!
|
|
471
|
-
|
|
472
|
-
## Project Structure
|
|
473
|
-
|
|
474
|
-
- \`apps/frontend/\` - React application with Vite
|
|
475
|
-
- \`apps/backend/\` - Cloudflare Workers API with Hono
|
|
476
|
-
- \`packages/api/\` - Shared API types
|
|
477
|
-
- \`packages/config/\` - Shared configuration
|
|
478
|
-
|
|
479
|
-
## Features
|
|
480
|
-
|
|
481
|
-
- 🚀 **Cloudflare Stack**: Workers, Pages, D1, KV, R2
|
|
482
|
-
- ⚡ **Modern Frontend**: React 19, TanStack Router & Query
|
|
483
|
-
- 🎨 **Styling**: Tailwind CSS v4 + shadcn/ui components
|
|
484
|
-
- 🔐 **Authentication**: Better Auth with social providers
|
|
485
|
-
- 🗄️ **Database**: D1 with Drizzle ORM
|
|
486
|
-
- 📡 **Type-Safe APIs**: tRPC for end-to-end type safety
|
|
487
|
-
- 🏗️ **Monorepo**: Turbo for fast builds and caching
|
|
488
|
-
|
|
489
|
-
## Development Commands
|
|
490
|
-
|
|
491
|
-
\`\`\`bash
|
|
492
|
-
${getPackageManagerCommand(packageManagerInfo)} dev # Start development servers
|
|
493
|
-
${getPackageManagerCommand(packageManagerInfo)} build # Build all packages
|
|
494
|
-
${getPackageManagerCommand(packageManagerInfo)} lint # Lint all packages
|
|
495
|
-
${getPackageManagerCommand(packageManagerInfo)} typecheck # Type check all packages
|
|
496
|
-
${getPackageManagerCommand(packageManagerInfo)} test # Run tests
|
|
497
|
-
\`\`\`
|
|
498
|
-
|
|
499
|
-
## Deployment
|
|
500
|
-
|
|
501
|
-
\`\`\`bash
|
|
502
|
-
# Deploy backend to Cloudflare Workers
|
|
503
|
-
${getPackageManagerCommand(packageManagerInfo)} deploy:backend
|
|
504
|
-
|
|
505
|
-
# Deploy frontend to Cloudflare Pages
|
|
506
|
-
${getPackageManagerCommand(packageManagerInfo)} deploy:frontend
|
|
507
|
-
|
|
508
|
-
# Deploy both
|
|
509
|
-
${getPackageManagerCommand(packageManagerInfo)} deploy:apps
|
|
510
|
-
\`\`\`
|
|
511
|
-
|
|
512
|
-
## Learn More
|
|
513
|
-
|
|
514
|
-
- [MVPKit Website](https://mvpkit.dev)
|
|
515
|
-
- [MVPKit Documentation](https://docs.mvpkit.dev)
|
|
516
|
-
- [MVPKit Examples](https://github.com/mvp-kit/core/examples)
|
|
517
|
-
`;
|
|
518
|
-
await fs.writeFile(path.join(targetDir, 'README.md'), readmeContent);
|
|
519
|
-
}
|
|
520
|
-
async function setupShadcn(targetDir, packageManager, options = {}) {
|
|
521
|
-
console.log();
|
|
522
|
-
console.log(drawBox([
|
|
523
|
-
'Setting up shadcn/ui component library',
|
|
524
|
-
'Configure your preferences, then we\'ll install essential components'
|
|
525
|
-
], 'Component Setup'));
|
|
526
|
-
console.log();
|
|
527
|
-
const frontendDir = path.join(targetDir, 'apps', 'frontend');
|
|
528
|
-
let componentSpinner = null;
|
|
605
|
+
/**
|
|
606
|
+
* Copy package manager specific files from _packageManagers directory
|
|
607
|
+
*/
|
|
608
|
+
export async function copyPackageManagerFiles(targetDir, variables, options = {}) {
|
|
609
|
+
const spinner = ora('Configuring package manager...').start();
|
|
529
610
|
try {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
611
|
+
const coreDir = await getTemplatePath(options);
|
|
612
|
+
const packageManagerDir = path.join(coreDir, '_packageManagers', variables.packageManager);
|
|
613
|
+
// Check if package manager directory exists
|
|
614
|
+
if (!fs.existsSync(packageManagerDir)) {
|
|
615
|
+
spinner.succeed('Package manager configuration complete');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Get all files from package manager directory
|
|
619
|
+
const files = await glob('**/*', {
|
|
620
|
+
cwd: packageManagerDir,
|
|
621
|
+
dot: true,
|
|
622
|
+
nodir: true,
|
|
623
|
+
absolute: false
|
|
624
|
+
});
|
|
625
|
+
// Copy each file to the target directory root
|
|
626
|
+
for (const file of files) {
|
|
627
|
+
const sourcePath = path.join(packageManagerDir, file);
|
|
628
|
+
const targetFile = path.join(targetDir, file);
|
|
629
|
+
// Ensure target directory exists
|
|
630
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
631
|
+
// Process template files if they have .template extension
|
|
632
|
+
if (file.endsWith('.template')) {
|
|
633
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
634
|
+
const processedContent = processTemplate(content, variables);
|
|
635
|
+
const finalTargetFile = targetFile.replace(/\.template$/, '');
|
|
636
|
+
await fs.writeFile(finalTargetFile, processedContent);
|
|
538
637
|
}
|
|
539
638
|
else {
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
console.log(chalk.gray('⌨️ Press Ctrl+C to cancel shadcn setup if needed'));
|
|
543
|
-
execSync('npx shadcn@latest init -s', {
|
|
544
|
-
cwd: frontendDir,
|
|
545
|
-
stdio: 'inherit',
|
|
546
|
-
killSignal: 'SIGKILL'
|
|
547
|
-
});
|
|
639
|
+
// Copy regular files as-is
|
|
640
|
+
await fs.copy(sourcePath, targetFile);
|
|
548
641
|
}
|
|
549
642
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
643
|
+
spinner.succeed(`Configured for ${variables.packageManager}`);
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
spinner.fail('Package manager configuration failed');
|
|
647
|
+
console.log(chalk.yellow(` Warning: ${error instanceof Error ? error.message : String(error)}`));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Install dependencies
|
|
652
|
+
*/
|
|
653
|
+
async function installDependencies(targetDir, packageManager) {
|
|
654
|
+
const spinner = ora('Installing packages...').start();
|
|
655
|
+
try {
|
|
656
|
+
const command = packageManager === 'bun' ? 'bun install' : 'pnpm install';
|
|
657
|
+
execSync(command, {
|
|
658
|
+
cwd: targetDir,
|
|
659
|
+
stdio: 'pipe'
|
|
660
|
+
});
|
|
661
|
+
spinner.succeed('All packages installed');
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
spinner.fail('Package installation failed');
|
|
665
|
+
console.log(chalk.yellow(`\n💡 Install packages manually: cd ${path.basename(targetDir)} && ${packageManager} install\n`));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Setup database with Drizzle ORM
|
|
670
|
+
*/
|
|
671
|
+
async function setupDatabase(apiPath, packageManager) {
|
|
672
|
+
const spinner = ora('Setting up database...').start();
|
|
673
|
+
try {
|
|
674
|
+
const pmCommand = packageManager === 'bun' ? 'bun' : 'pnpm';
|
|
675
|
+
// Initialize local database
|
|
676
|
+
execSync(`${pmCommand} run db:init`, {
|
|
677
|
+
cwd: apiPath,
|
|
678
|
+
stdio: 'pipe',
|
|
679
|
+
timeout: 30000
|
|
680
|
+
});
|
|
681
|
+
// Generate database schema
|
|
682
|
+
execSync(`${pmCommand} run db:generate`, {
|
|
683
|
+
cwd: apiPath,
|
|
684
|
+
stdio: 'pipe',
|
|
685
|
+
timeout: 60000
|
|
686
|
+
});
|
|
687
|
+
// Run local migrations
|
|
688
|
+
execSync(`${pmCommand} run db:migrate:local`, {
|
|
689
|
+
cwd: apiPath,
|
|
690
|
+
stdio: 'pipe',
|
|
691
|
+
timeout: 60000
|
|
692
|
+
});
|
|
693
|
+
spinner.succeed('Database setup completed');
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
spinner.fail('Database setup failed');
|
|
697
|
+
console.log(chalk.yellow(`\n💡 Setup database manually: cd ${path.basename(apiPath)} && ${packageManager} run db:generate && ${packageManager} run db:migrate:local\n`));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Run lint:fix command if available
|
|
702
|
+
*/
|
|
703
|
+
async function runLintFixCommand(targetDir, packageManager) {
|
|
704
|
+
const spinner = ora('Fixing code formatting...').start();
|
|
705
|
+
try {
|
|
706
|
+
const pmCommand = packageManager === 'pnpm' ? 'pnpm' : 'bun';
|
|
707
|
+
execSync(`${pmCommand} run lint:fix`, {
|
|
708
|
+
cwd: targetDir,
|
|
709
|
+
stdio: 'pipe',
|
|
710
|
+
timeout: 60000 // 60 second timeout
|
|
711
|
+
});
|
|
712
|
+
spinner.succeed('Code formatted and linted');
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
// If lint:fix fails or doesn't exist, try just lint
|
|
716
|
+
try {
|
|
717
|
+
const pmCommand = packageManager === 'pnpm' ? 'pnpm' : 'bun';
|
|
718
|
+
execSync(`${pmCommand} run lint`, {
|
|
719
|
+
cwd: targetDir,
|
|
720
|
+
stdio: 'pipe',
|
|
721
|
+
timeout: 60000
|
|
722
|
+
});
|
|
723
|
+
spinner.succeed('Code formatting verified');
|
|
560
724
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
for (const component of components) {
|
|
565
|
-
try {
|
|
566
|
-
execSync(`npx shadcn@latest add ${component} -s --overwrite --yes`, {
|
|
567
|
-
cwd: frontendDir,
|
|
568
|
-
stdio: 'inherit'
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
catch {
|
|
572
|
-
try {
|
|
573
|
-
execSync(`npx shadcn@latest add ${component} -s --yes`, {
|
|
574
|
-
cwd: frontendDir,
|
|
575
|
-
stdio: 'inherit'
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
580
|
-
console.log(chalk.yellow(`⚠️ Failed to install ${component}`));
|
|
581
|
-
console.log(chalk.gray(` Error: ${errorMessage}`));
|
|
582
|
-
}
|
|
583
|
-
}
|
|
725
|
+
catch (lintError) {
|
|
726
|
+
// If no lint command exists, skip silently
|
|
727
|
+
spinner.succeed('Code formatting completed');
|
|
584
728
|
}
|
|
585
|
-
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Initialize git repository with commit (after lint:fix)
|
|
733
|
+
*/
|
|
734
|
+
async function initializeGitWithCommit(targetDir) {
|
|
735
|
+
const spinner = ora('Initializing version control...').start();
|
|
736
|
+
try {
|
|
737
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
738
|
+
execSync('git add .', { cwd: targetDir, stdio: 'pipe' });
|
|
739
|
+
execSync('git commit -m "Initial commit from MVPKit Create CLI"', { cwd: targetDir, stdio: 'pipe' });
|
|
740
|
+
spinner.succeed('Version control initialized with initial commit');
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
spinner.fail('Version control initialization failed');
|
|
744
|
+
console.log(chalk.yellow('\n💡 Initialize version control manually if needed\n'));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Initialize git repository (legacy function for compatibility)
|
|
749
|
+
*/
|
|
750
|
+
async function initializeGit(targetDir) {
|
|
751
|
+
const spinner = ora('Initializing version control...').start();
|
|
752
|
+
try {
|
|
753
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
754
|
+
execSync('git add .', { cwd: targetDir, stdio: 'pipe' });
|
|
755
|
+
execSync('git commit -m "Initial commit"', { cwd: targetDir, stdio: 'pipe' });
|
|
756
|
+
spinner.succeed('Version control initialized');
|
|
586
757
|
}
|
|
587
758
|
catch (error) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
console.log(chalk.yellow('💡 Run manually: npx shadcn@latest init'));
|
|
591
|
-
console.log(chalk.gray(` Error: ${errorMessage}`));
|
|
759
|
+
spinner.fail('Version control initialization failed');
|
|
760
|
+
console.log(chalk.yellow('\n💡 Initialize version control manually if needed\n'));
|
|
592
761
|
}
|
|
593
762
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Print next steps
|
|
765
|
+
*/
|
|
766
|
+
function printNextSteps(projectName, options, packageManager) {
|
|
767
|
+
const pmCommand = packageManager === 'bun' ? 'bun' : 'pnpm';
|
|
768
|
+
console.log('\n' + chalk.cyan('🚀 Get Started'));
|
|
769
|
+
console.log(chalk.white.dim('═'.repeat(50)));
|
|
770
|
+
// Step-by-step instructions
|
|
771
|
+
console.log(chalk.bold('Next steps:'));
|
|
772
|
+
console.log(chalk.green(` 1. cd ${projectName}`));
|
|
773
|
+
if (options.install === false) {
|
|
774
|
+
console.log(chalk.green(` 2. ${pmCommand} install`));
|
|
775
|
+
console.log(chalk.green(` 3. ${pmCommand} dev`));
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
console.log(chalk.green(` 2. ${pmCommand} dev`));
|
|
779
|
+
}
|
|
780
|
+
console.log(chalk.green(` 3. Open http://localhost:3000`));
|
|
781
|
+
// What's included section
|
|
782
|
+
console.log('\n' + chalk.whiteBright.bold('🎁 What\'s Included:'));
|
|
783
|
+
const features = [
|
|
784
|
+
'⚡ Full-stack TypeScript with hot reload',
|
|
785
|
+
'🗄️ Database with Drizzle ORM + migrations',
|
|
786
|
+
'🎨 shadcn/ui components with new-york theme',
|
|
787
|
+
'🔐 Authentication with better-auth',
|
|
788
|
+
'📦 Monorepo structure (apps + packages)',
|
|
789
|
+
'🔧 ESLint, Prettier, and TypeScript configured'
|
|
790
|
+
];
|
|
791
|
+
if (options.componentPack && options.componentPack === 'all') {
|
|
792
|
+
features.splice(2, 1, `🎨 shadcn/ui complete component library + new-york theme`);
|
|
793
|
+
}
|
|
794
|
+
else if (options.componentPack) {
|
|
795
|
+
features.splice(2, 1, `🎨 shadcn/ui essentials component pack + new-york theme`);
|
|
796
|
+
}
|
|
797
|
+
features.forEach(feature => {
|
|
798
|
+
console.log(chalk.white(` ${feature}`));
|
|
799
|
+
});
|
|
800
|
+
// Quick tips
|
|
801
|
+
console.log('\n' + chalk.whiteBright.bold('💡 Quick Tips:'));
|
|
802
|
+
console.log(chalk.white(' • Database is already set up and migrated'));
|
|
803
|
+
console.log(chalk.white(' • Add more components: npx shadcn@latest add <component>'));
|
|
804
|
+
console.log(chalk.white(' • Customize UI theme in packages/ui/src/styles.css'));
|
|
805
|
+
console.log(chalk.white(' • API routes are in apps/api/src/routes/'));
|
|
806
|
+
console.log('\n' + chalk.bold('📚 Learn More:'));
|
|
807
|
+
console.log(chalk.blue(' https://docs.mvpkit.dev'));
|
|
808
|
+
console.log(chalk.white(' https://ui.shadcn.com (for UI components)'));
|
|
809
|
+
console.log(chalk.white(' https://orm.drizzle.team (for database)'));
|
|
810
|
+
console.log(chalk.white(''));
|
|
628
811
|
}
|
|
629
812
|
//# sourceMappingURL=create.js.map
|