@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/dist/create.js CHANGED
@@ -1,629 +1,812 @@
1
1
  import fs from 'fs-extra';
2
- import path from 'path';
3
- import { execSync } from 'child_process';
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 inquirer from 'inquirer';
7
- import { glob } from 'glob';
8
- // Enhanced Box System
9
- const BOX_WIDTH = 72;
10
- const BOX_INDENT = ' ';
11
- function drawBox(content, title) {
12
- const lines = [];
13
- // Top border
14
- lines.push(BOX_INDENT + '┌' + '─'.repeat(BOX_WIDTH - 2) + '┐');
15
- // Title if provided
16
- if (title) {
17
- lines.push(drawBoxLine(title, true));
18
- lines.push(drawBoxLine(''));
19
- }
20
- // Content lines with wrapping
21
- content.forEach(line => {
22
- const wrappedLines = wrapText(line, BOX_WIDTH - 4);
23
- wrappedLines.forEach(wrappedLine => {
24
- lines.push(drawBoxLine(wrappedLine));
25
- });
26
- });
27
- // Add empty line if we have content
28
- if (content.length > 0) {
29
- lines.push(drawBoxLine(''));
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
- // Bottom border
32
- lines.push(BOX_INDENT + '└' + '─'.repeat(BOX_WIDTH - 2) + '┘');
33
- return lines.join('\n');
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
- function wrapText(text, maxWidth) {
36
- if (text.length <= maxWidth) {
37
- return [text];
38
- }
39
- const words = text.split(' ');
40
- const lines = [];
41
- let currentLine = '';
42
- for (const word of words) {
43
- if ((currentLine + (currentLine ? ' ' : '') + word).length <= maxWidth) {
44
- currentLine += (currentLine ? ' ' : '') + word;
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
- if (currentLine)
48
- lines.push(currentLine);
49
- currentLine = word;
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
- function drawBoxLine(content, isTitle = false) {
57
- const availableWidth = BOX_WIDTH - 4; // Account for borders and spaces
58
- const padding = availableWidth - content.length;
59
- const coloredText = isTitle ? chalk.bold.cyan(content) : chalk.white(content);
60
- return BOX_INDENT + '│ ' + coloredText + ' '.repeat(padding) + ' │';
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
- import { generateTemplateVariables, getCoreRepositoryPath, shouldSkipFile, DEFAULT_SKIP_PATTERNS, transformFileName } from './utils.js';
63
- import { getFileTransformFunction, transformTemplateFileName } from './template.js';
64
- import { setupDatabase } from './database.js';
65
- import { TemplateDownloader, TEMPLATE_REGISTRY } from './template-downloader.js';
66
- function detectPackageManager(dir) {
67
- // Check how CLI was invoked (highest priority)
68
- const invocation = detectInvocationMethod();
69
- if (invocation) {
70
- return invocation;
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
- function getPackageVersion(pm) {
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 result = execSync(`${pm} --version`, { encoding: 'utf8', stdio: 'pipe' });
85
- return result.trim();
331
+ const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
332
+ return { packageManager: 'bun', version: bunVersion };
86
333
  }
87
334
  catch {
88
- return '10.14.0';
335
+ const pnpmVersion = await getPnpmVersion();
336
+ return { packageManager: 'pnpm', version: pnpmVersion };
89
337
  }
90
338
  }
91
- function detectInvocationMethod() {
92
- // Check process.execArgv and process.argv to see how we were invoked
93
- const execPath = process.execPath;
94
- const argv = process.argv;
95
- const userAgent = process.env.npm_config_user_agent || '';
96
- // Check if invoked via bunx
97
- if (execPath.includes('bun') || userAgent.startsWith('bun')) {
98
- return { pm: 'bun', version: extractVersion(userAgent) || 'latest' };
99
- }
100
- // Check argv for package manager indicators
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
- function extractVersion(userAgent) {
121
- // Extract version from user agent string
122
- // Examples: "npm/8.19.2 node/v18.17.0 linux x64", "pnpm/8.6.0", "bun/1.0.0"
123
- const match = userAgent.match(/^(npm|pnpm|bun)\/([^\s]+)/);
124
- return match ? match[2] : '10.14.0';
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
- function getPackageManagerCommand(pmInfo) {
127
- const commands = {
128
- npm: 'npm',
129
- pnpm: 'pnpm',
130
- bun: 'bun'
131
- };
132
- return commands[pmInfo.pm];
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.resolve(projectName);
136
- // Show MVPKit branding
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
- // Always use core template (only template available)
152
- const templateName = 'core';
153
- const template = TEMPLATE_REGISTRY[templateName];
154
- if (!template) {
155
- throw new Error(`Core template not found`);
156
- }
157
- // Detect package manager (we need this early for setup files)
158
- const packageManagerInfo = detectPackageManager(process.cwd());
159
- const pmCommand = getPackageManagerCommand(packageManagerInfo);
160
- // Show template info
161
- console.log(drawBox([
162
- `🆓 ${template.displayName}`,
163
- template.description,
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
- await copyAndTransformTemplate(targetDir, variables, templatePath, packageManagerInfo);
196
- copySpinner.succeed('Template files copied and transformed');
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
- copySpinner.fail('Failed to copy template files');
200
- const errorMessage = error instanceof Error ? error.message : String(error);
201
- console.error(chalk.red('Error details:'), errorMessage);
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
- catch (error) {
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
- // Step 4: Setup shadcn/ui
227
- await setupShadcn(targetDir, targetPackageManagerInfo.pm, options);
228
- // Step 5: Finalize project
229
- if (options.install !== false) {
230
- const finalizeSpinner = ora('Finalizing...').start();
231
- // Build project
232
- try {
233
- execSync(`${targetPmCommand} build`, { cwd: targetDir, stdio: 'ignore' });
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
- catch (error) {
236
- finalizeSpinner.fail('Build failed');
237
- const errorMessage = error instanceof Error ? error.message : String(error);
238
- console.log(chalk.red(`💡 Build failed: ${errorMessage}`));
239
- console.log(chalk.yellow(' Check the error messages above and fix any issues'));
240
- throw new Error(`Build failed: ${errorMessage}`);
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
- // Format and lint code
243
- try {
244
- execSync(`${targetPmCommand} format:fix`, { cwd: targetDir, stdio: 'ignore' });
245
- execSync(`${targetPmCommand} lint:fix`, { cwd: targetDir, stdio: 'ignore' });
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
- catch (error) {
248
- finalizeSpinner.warn('Code formatting/linting skipped (optional)');
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
- // Initialize git (if requested)
251
- if (options.git) {
252
- try {
253
- execSync('git init', { cwd: targetDir, stdio: 'ignore' });
254
- execSync('git add .', { cwd: targetDir, stdio: 'ignore' });
255
- execSync('git commit -m "Initial commit from MVPKit"', { cwd: targetDir, stdio: 'ignore' });
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
- catch (error) {
258
- finalizeSpinner.warn('Git initialization failed (optional)');
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
- finalizeSpinner.succeed('Setup completed');
262
- }
263
- // Success message
264
- console.log();
265
- console.log(drawBox([
266
- `🎉 ${variables.projectDisplayName} is ready!`,
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
- async function runInteractivePrompts(projectName, variables, options) {
275
- console.log();
276
- console.log(drawBox([
277
- 'Just a few quick questions to get you started'
278
- ], 'Configure Your Project'));
279
- console.log();
280
- const answers = await inquirer.prompt([
281
- {
282
- type: 'input',
283
- name: 'description',
284
- message: `Brief description for "${projectName}":`,
285
- default: variables.projectDescription
286
- },
287
- {
288
- type: 'input',
289
- name: 'domain',
290
- message: 'Production domain name (e.g. myapp.com, leave empty to skip):',
291
- default: variables.domainName || ''
292
- },
293
- {
294
- type: 'input',
295
- name: 'packageScope',
296
- message: 'Package namespace (e.g. @yourcompany):',
297
- default: variables.packageScope,
298
- validate: (input) => {
299
- if (!input.startsWith('@')) {
300
- return 'Must start with @ (e.g. @yourcompany)';
301
- }
302
- return true;
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
- async function copyAndTransformTemplate(targetDir, variables, templatePath, packageManagerInfo) {
338
- const coreDir = templatePath || getCoreRepositoryPath();
339
- if (!fs.existsSync(coreDir)) {
340
- throw new Error(`Template directory not found at ${coreDir}`);
341
- }
342
- // Get all files from core directory with optimized glob pattern
343
- const files = await glob('**/*', {
344
- cwd: coreDir,
345
- dot: true,
346
- nodir: true,
347
- absolute: false
348
- });
349
- // Process each file
350
- for (const sourceFile of files) {
351
- const sourcePath = path.join(coreDir, sourceFile);
352
- const relativePath = sourceFile;
353
- // Create package-manager specific skip patterns
354
- const skipPatterns = [...DEFAULT_SKIP_PATTERNS];
355
- if (variables.packageManager === 'bun') {
356
- skipPatterns.push('pnpm-workspace.yaml');
357
- }
358
- // Skip files we don't want to copy
359
- if (shouldSkipFile(relativePath, skipPatterns)) {
360
- continue;
361
- }
362
- // Check if this is a template file (.template/.tpl)
363
- const isTemplateFile = relativePath.endsWith('.template') || relativePath.endsWith('.tpl');
364
- // Transform the file path if needed
365
- let targetRelativePath;
366
- if (isTemplateFile) {
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
- // Write transformed content
407
- await fs.writeFile(targetFile, transformedContent);
594
+ // Copy regular files as-is
595
+ await fs.copy(sourcePath, targetFile);
408
596
  }
409
597
  }
410
- catch (error) {
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
- // Create additional setup files
416
- if (packageManagerInfo) {
417
- await createSetupFiles(targetDir, variables, packageManagerInfo);
600
+ catch (error) {
601
+ spinner.fail('Project structure creation failed');
602
+ throw error;
418
603
  }
419
604
  }
420
- async function createSetupFiles(targetDir, variables, packageManagerInfo) {
421
- // Create .env.example files with proper variable names
422
- const frontendEnvExample = `# Frontend Environment Variables
423
- VITE_API_URL=http://localhost:8787
424
- VITE_APP_NAME="${variables.projectDisplayName}"
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
- // Initialize shadcn
531
- try {
532
- if (options.interactive === false) {
533
- // Non-interactive mode with zinc theme
534
- execSync('npx shadcn@latest init -s -b zinc --yes', {
535
- cwd: frontendDir,
536
- stdio: 'pipe'
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
- // Interactive mode - temporarily restore cursor and handle signals
541
- process.stdout.write('\x1B[?25h'); // Show cursor
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
- catch (error) {
551
- const errorMessage = error instanceof Error ? error.message : String(error);
552
- // Check if user cancelled (Ctrl+C)
553
- if (errorMessage.includes('SIGINT') || errorMessage.includes('cancelled')) {
554
- console.log(chalk.yellow('\n🛑 Cancelled'));
555
- process.exit(0);
556
- }
557
- console.log(chalk.yellow('⚠️ shadcn setup failed, skipping component installation'));
558
- console.log(chalk.gray(` ${errorMessage}`));
559
- return; // Exit shadcn setup entirely if init failed
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
- // Install shadcn components
562
- componentSpinner = ora('Installing components...').start();
563
- const components = ['alert', 'badge', 'button', 'card', 'input', 'label'];
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
- componentSpinner?.succeed('Components installed');
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
- componentSpinner?.fail('Setup failed');
589
- const errorMessage = error instanceof Error ? error.message : String(error);
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
- function printNextSteps(projectName, options, dbSetupCompleted = false, packageManager = 'pnpm') {
595
- const pmCommand = getPackageManagerCommand({ pm: packageManager, version: packageManager === 'pnpm' ? '10.14.0' : 'latest' });
596
- // Build next steps list
597
- const steps = [`1. cd ${projectName}`];
598
- let step = 2;
599
- if (!options.install) {
600
- steps.push(`${step}. ${pmCommand} install`);
601
- step++;
602
- }
603
- steps.push(`${step}. cp apps/frontend/.env.example apps/frontend/.env.local`);
604
- step++;
605
- steps.push(`${step}. cp apps/backend/.dev.vars.example apps/backend/.dev.vars`);
606
- step++;
607
- if (!dbSetupCompleted) {
608
- steps.push(`${step}. cd apps/backend`);
609
- step++;
610
- steps.push(`${step}. ${pmCommand} db:generate`);
611
- step++;
612
- steps.push(`${step}. ${pmCommand} db:migrate:local`);
613
- step++;
614
- steps.push(`${step}. cd ../..`);
615
- step++;
616
- }
617
- steps.push(`${step}. ${pmCommand} dev`);
618
- console.log(drawBox(steps, 'Next Steps'));
619
- console.log();
620
- console.log(drawBox([
621
- '🌐 Website: https://mvpkit.dev',
622
- '📖 Documentation: https://docs.mvpkit.dev',
623
- '🔗 Examples: https://github.com/mvp-kit/core/examples'
624
- ], 'Resources'));
625
- console.log();
626
- console.log(chalk.bold.green('🚀 Happy building with MVPKit!'));
627
- console.log();
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