@mvp-kit/create 0.0.10 → 0.0.12

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,921 @@
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
+ import { fileURLToPath } from 'node:url';
12
+ const SUPPORTED_TEMPLATES = ['core', 'saas'];
13
+ function validateTemplateName(template) {
14
+ if (!SUPPORTED_TEMPLATES.includes(template)) {
15
+ throw new Error(`Unsupported template "${template}". Supported templates: ${SUPPORTED_TEMPLATES.join(', ')}`);
30
16
  }
31
- // Bottom border
32
- lines.push(BOX_INDENT + '└' + '─'.repeat(BOX_WIDTH - 2) + '┘');
33
- return lines.join('\n');
34
17
  }
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;
18
+ /**
19
+ * Get path to template directory
20
+ * Supports multiple ways to specify template location:
21
+ * 1. --template-path CLI option (passed via options.templatePath)
22
+ * 2. MVPKIT_TEMPLATE_PATH environment variable
23
+ * 3. Download from GitHub releases using --template option
24
+ */
25
+ async function getTemplatePath(options = {}) {
26
+ // 1. CLI option takes highest priority
27
+ if (options.templatePath) {
28
+ const templatePath = path.resolve(options.templatePath);
29
+ if (fs.existsSync(templatePath)) {
30
+ return templatePath;
45
31
  }
46
- else {
47
- if (currentLine)
48
- lines.push(currentLine);
49
- currentLine = word;
32
+ throw new Error(`Template path not found: ${templatePath}`);
33
+ }
34
+ // 2. Environment variable
35
+ if (process.env.MVPKIT_TEMPLATE_PATH) {
36
+ const templatePath = path.resolve(process.env.MVPKIT_TEMPLATE_PATH);
37
+ if (fs.existsSync(templatePath)) {
38
+ return templatePath;
50
39
  }
40
+ throw new Error(`Template path from MVPKIT_TEMPLATE_PATH not found: ${templatePath}`);
51
41
  }
52
- if (currentLine)
53
- lines.push(currentLine);
54
- return lines;
55
- }
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) + ' │';
61
- }
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' };
42
+ // 3. Local monorepo fallback for core/saas templates (dev workflow)
43
+ const template = options.template || 'core';
44
+ if (SUPPORTED_TEMPLATES.includes(template)) {
45
+ const cliDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
46
+ const monorepoTemplatePath = path.resolve(cliDir, '..', template);
47
+ if (fs.existsSync(monorepoTemplatePath)) {
48
+ return monorepoTemplatePath;
49
+ }
50
+ }
51
+ // 4. Download from GitHub based on template option
52
+ const cacheDir = path.join(os.tmpdir(), 'mvpkit-template-cache');
53
+ const templateDir = path.join(cacheDir, template);
54
+ // Check if caching is disabled or if we don't have a cached template
55
+ const useCache = options.cache !== false;
56
+ const hasCachedTemplate = useCache && fs.existsSync(templateDir) && fs.readdirSync(templateDir).length > 0;
57
+ if (hasCachedTemplate) {
58
+ return templateDir;
59
+ }
60
+ // Download the template (this will clear the directory if it exists)
61
+ return await downloadTemplate(template, templateDir);
81
62
  }
82
- function getPackageVersion(pm) {
63
+ /**
64
+ * Read template configuration from template.json
65
+ */
66
+ async function readTemplateConfig(templateDir) {
83
67
  try {
84
- const result = execSync(`${pm} --version`, { encoding: 'utf8', stdio: 'pipe' });
85
- return result.trim();
68
+ const configPath = path.join(templateDir, 'template.json');
69
+ if (fs.existsSync(configPath)) {
70
+ const configContent = await fs.readFile(configPath, 'utf-8');
71
+ return JSON.parse(configContent);
72
+ }
86
73
  }
87
- catch {
88
- return '10.14.0';
74
+ catch (error) {
75
+ console.log(chalk.yellow('⚠️ Could not read template configuration'));
89
76
  }
90
- }
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
77
  return null;
119
78
  }
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';
125
- }
126
- function getPackageManagerCommand(pmInfo) {
127
- const commands = {
128
- npm: 'npm',
129
- pnpm: 'pnpm',
130
- bun: 'bun'
131
- };
132
- return commands[pmInfo.pm];
133
- }
134
- 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
148
- if (fs.existsSync(targetDir)) {
149
- throw new Error(`Directory ${projectName} already exists`);
150
- }
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);
79
+ /**
80
+ * Collect missing template variables interactively
81
+ */
82
+ async function collectVariables(variables, options) {
83
+ // If non-interactive mode, use simple detection without prompts
84
+ if (options.interactive === false) {
85
+ const { packageManager, version: packageManagerVersion } = await detectPackageManager();
86
+ return {
87
+ ...variables,
88
+ packageManager,
89
+ packageManagerVersion,
90
+ componentPack: options.componentPack || 'basic',
91
+ git: options.git !== false
92
+ };
174
93
  }
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();
94
+ const normalizeHandle = (handle) => {
95
+ if (!handle)
96
+ return undefined;
97
+ const trimmed = handle.trim();
98
+ if (!trimmed)
99
+ return undefined;
100
+ return trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
101
+ };
102
+ const toSocialUrl = (platform, handle) => {
103
+ const normalized = normalizeHandle(handle);
104
+ if (!normalized)
105
+ return undefined;
106
+ const username = normalized.slice(1);
107
+ if (!username)
108
+ return undefined;
109
+ if (platform === 'tiktok')
110
+ return `https://tiktok.com/@${username}`;
111
+ if (platform === 'x')
112
+ return `https://x.com/${username}`;
113
+ if (platform === 'instagram')
114
+ return `https://instagram.com/${username}`;
115
+ return `https://github.com/${username}`;
116
+ };
117
+ const fieldsToCollect = [
118
+ // 1. Template selection (if not passed via CLI)
119
+ {
120
+ name: 'template',
121
+ message: 'Select template',
122
+ type: !options.template && !options.templatePath ? 'select' : null,
123
+ choices: [
124
+ { title: 'Core', value: 'core' },
125
+ { title: 'SaaS', value: 'saas' }
126
+ ],
127
+ initial: 0
128
+ },
129
+ // 2. Project description
130
+ {
131
+ name: 'description',
132
+ message: 'Project description',
133
+ type: 'text',
134
+ initial: options.description || `A full-stack application built with MVPKit`
135
+ },
136
+ // 3. Domain name
137
+ {
138
+ name: 'domain',
139
+ message: 'Website domain (for deployment)',
140
+ type: 'text',
141
+ initial: options.domain || `${variables.name}.localhost`
142
+ },
143
+ // 4. Tagline
144
+ {
145
+ name: 'tagline',
146
+ message: 'Tagline',
147
+ type: 'text',
148
+ initial: variables.tagline || 'Build and deploy MVPs faster.'
149
+ },
150
+ // 5. Component pack selection
151
+ {
152
+ name: 'componentPack',
153
+ message: 'Choose component pack',
154
+ type: !options.componentPack ? 'select' : null,
155
+ choices: [
156
+ { title: 'Essentials', value: 'basic' },
157
+ { title: 'All', value: 'all' }
158
+ ],
159
+ initial: 0
160
+ },
161
+ // 6. SaaS social handles
162
+ {
163
+ name: 'socialGithubHandle',
164
+ message: 'GitHub handle (@username)',
165
+ type: (_prev, values) => (options.template || values.template || variables.template) === 'saas' ? 'text' : null,
166
+ initial: variables.social?.github?.handle || '@mvp-kit'
167
+ },
168
+ {
169
+ name: 'socialXHandle',
170
+ message: 'X handle (@username)',
171
+ type: (_prev, values) => (options.template || values.template || variables.template) === 'saas' ? 'text' : null,
172
+ initial: variables.social?.x?.handle || '@mvpkithq'
173
+ },
174
+ {
175
+ name: 'socialInstagramHandle',
176
+ message: 'Instagram handle (@username)',
177
+ type: (_prev, values) => (options.template || values.template || variables.template) === 'saas' ? 'text' : null,
178
+ initial: variables.social?.instagram?.handle || '@mvpkit'
179
+ },
180
+ {
181
+ name: 'socialTiktokHandle',
182
+ message: 'TikTok handle (@username)',
183
+ type: (_prev, values) => (options.template || values.template || variables.template) === 'saas' ? 'text' : null,
184
+ initial: variables.social?.tiktok?.handle || '@mvpkittok'
185
+ },
186
+ // 7. Git initialization
187
+ {
188
+ name: 'git',
189
+ message: 'Initialize git repository?',
190
+ type: options.git === undefined ? 'confirm' : null,
191
+ initial: options.git !== false,
192
+ },
193
+ // 8. Package manager (last, after user sees what will be installed)
194
+ {
195
+ name: 'packageManager',
196
+ message: 'Choose package manager',
197
+ type: 'select',
198
+ choices: [
199
+ { title: 'pnpm [stable]', value: 'pnpm' },
200
+ { title: 'bun', value: 'bun' }
201
+ ],
202
+ initial: 0
203
+ }
204
+ ];
194
205
  try {
195
- await copyAndTransformTemplate(targetDir, variables, templatePath, packageManagerInfo);
196
- copySpinner.succeed('Template files copied and transformed');
206
+ // Check if we can use prompts (TTY available)
207
+ if (!process.stdin.isTTY) {
208
+ console.log(chalk.yellow('⚠️ Non-interactive environment detected, using defaults'));
209
+ const { packageManager, version: packageManagerVersion } = await detectPackageManager();
210
+ return {
211
+ ...variables,
212
+ packageManager,
213
+ packageManagerVersion
214
+ };
215
+ }
216
+ console.log(chalk.cyan('🔧 Project Configuration'));
217
+ console.log(chalk.white.dim('═'.repeat(50)));
218
+ let responses;
219
+ try {
220
+ responses = await prompts(fieldsToCollect, {
221
+ onCancel: () => {
222
+ console.log(chalk.yellow('\n⚠️ Operation cancelled by user'));
223
+ process.exit(1);
224
+ }
225
+ });
226
+ }
227
+ catch (promptError) {
228
+ console.log(chalk.yellow('⚠️ Interactive prompts failed, using defaults'));
229
+ console.log(chalk.white.dim(` Error: ${promptError instanceof Error ? promptError.message : 'Unknown error'}`));
230
+ const { packageManager, version: packageManagerVersion } = await detectPackageManager();
231
+ return {
232
+ ...variables,
233
+ packageManager,
234
+ packageManagerVersion
235
+ };
236
+ }
237
+ // Get package manager version
238
+ let packageManagerVersion = '9.0.0'; // default
239
+ if (responses.packageManager === 'bun') {
240
+ try {
241
+ packageManagerVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
242
+ }
243
+ catch {
244
+ packageManagerVersion = '1.0.0';
245
+ }
246
+ }
247
+ else {
248
+ try {
249
+ packageManagerVersion = execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
250
+ }
251
+ catch {
252
+ packageManagerVersion = '9.0.0';
253
+ }
254
+ }
255
+ const socialGithubHandle = normalizeHandle(responses.socialGithubHandle || variables.social?.github?.handle);
256
+ const socialXHandle = normalizeHandle(responses.socialXHandle || variables.social?.x?.handle);
257
+ const socialInstagramHandle = normalizeHandle(responses.socialInstagramHandle || variables.social?.instagram?.handle);
258
+ const socialTiktokHandle = normalizeHandle(responses.socialTiktokHandle || variables.social?.tiktok?.handle);
259
+ return {
260
+ ...variables,
261
+ template: responses.template || options.template || variables.template,
262
+ description: responses.description || variables.description,
263
+ domain: responses.domain || variables.domain,
264
+ tagline: responses.tagline || variables.tagline,
265
+ social: {
266
+ github: {
267
+ handle: socialGithubHandle,
268
+ url: toSocialUrl('github', socialGithubHandle),
269
+ },
270
+ x: {
271
+ handle: socialXHandle,
272
+ url: toSocialUrl('x', socialXHandle),
273
+ },
274
+ instagram: {
275
+ handle: socialInstagramHandle,
276
+ url: toSocialUrl('instagram', socialInstagramHandle),
277
+ },
278
+ tiktok: {
279
+ handle: socialTiktokHandle,
280
+ url: toSocialUrl('tiktok', socialTiktokHandle),
281
+ },
282
+ },
283
+ packageManager: responses.packageManager || 'pnpm',
284
+ packageManagerVersion,
285
+ componentPack: responses.componentPack || options.componentPack,
286
+ git: responses.git !== undefined ? responses.git : (options.git !== false)
287
+ };
197
288
  }
198
289
  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)
290
+ console.log(chalk.yellow('\n⚠️ Prompt collection failed, using defaults'));
291
+ const { packageManager, version: packageManagerVersion } = await detectPackageManager();
292
+ return {
293
+ ...variables,
294
+ packageManager,
295
+ packageManagerVersion
296
+ };
297
+ }
298
+ }
299
+ /**
300
+ * Complete project setup after template is copied
301
+ * Runs the same steps for both CLI and TUI modes
302
+ */
303
+ async function completeProjectSetup(targetDir, variables, options) {
304
+ const packageManager = variables.packageManager;
305
+ // Step 1: Copy package manager specific files
306
+ await copyPackageManagerFiles(targetDir, variables, options);
307
+ // Step 2: Install dependencies
208
308
  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');
309
+ await installDependencies(targetDir, packageManager);
310
+ }
311
+ // Step 3: Setup database
312
+ const apiPath = path.join(targetDir, 'services', 'api');
313
+ if (fs.existsSync(apiPath) && options.install !== false) {
314
+ await setupDatabase(apiPath, packageManager);
315
+ }
316
+ // Step 4: Initialize shadcn in UI package
317
+ const uiPath = path.join(targetDir, 'packages', 'ui');
318
+ const hasPreconfiguredUI = fs.existsSync(path.join(uiPath, 'components.json')) &&
319
+ fs.existsSync(path.join(uiPath, 'src', 'components', 'ui'));
320
+ if (fs.existsSync(uiPath)) {
321
+ if (!hasPreconfiguredUI) {
322
+ await initShadcnInUIPackage(uiPath, 'new-york', options.interactive !== false);
213
323
  }
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}`));
324
+ }
325
+ // Step 5: Install component pack
326
+ const componentPack = variables.componentPack;
327
+ if (componentPack && fs.existsSync(uiPath)) {
328
+ // The core template already includes the baseline "basic" set.
329
+ const shouldInstallPack = componentPack === 'all' || !hasPreconfiguredUI;
330
+ if (shouldInstallPack) {
331
+ const templateConfig = await readTemplateConfig(targetDir);
332
+ await installComponentPack(uiPath, componentPack, options.interactive !== false, templateConfig);
219
333
  }
220
334
  }
221
- // Step 3: Setup database
222
- let dbSetupCompleted = false;
223
- if (options.setupDb !== false) {
224
- dbSetupCompleted = await setupDatabase(targetDir, variables);
335
+ // Step 6: Cleanup tasks
336
+ await cleanup(targetDir, packageManager);
337
+ // Step 7: Initialize git at the end
338
+ const shouldInitGit = variables.git;
339
+ if (shouldInitGit !== false) {
340
+ await initializeGitWithCommit(targetDir);
225
341
  }
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
342
+ }
343
+ async function cleanup(targetDir, packageManager) {
344
+ // Only inherit theme name from UI to web
345
+ const uiPath = path.join(targetDir, 'packages', 'ui');
346
+ const webPath = path.join(targetDir, 'apps', 'web');
347
+ if (fs.existsSync(uiPath) && fs.existsSync(webPath)) {
348
+ await inheritUIConfigToWeb(uiPath, webPath);
349
+ }
350
+ // Run lint fix
351
+ await runLintFixCommand(targetDir, packageManager);
352
+ }
353
+ /**
354
+ * Detect package manager based on how CLI was invoked
355
+ * - npx: Ask user to choose
356
+ * - pnpm create/pnpm dlx: Use pnpm
357
+ * - bunx: Use bun
358
+ */
359
+ async function detectPackageManager() {
360
+ // Check how the CLI was invoked using environment variables and process.argv
361
+ const userAgent = process.env.npm_config_user_agent || '';
362
+ const invocation = process.argv.join(' ');
363
+ // Check user agent for package manager detection
364
+ if (userAgent.includes('bun/')) {
232
365
  try {
233
- execSync(`${targetPmCommand} build`, { cwd: targetDir, stdio: 'ignore' });
366
+ const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
367
+ return { packageManager: 'bun', version: bunVersion };
234
368
  }
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}`);
369
+ catch {
370
+ console.log(chalk.yellow('⚠️ Bun not available, falling back to pnpm'));
371
+ const pnpmVersion = await getPnpmVersion();
372
+ return { packageManager: 'pnpm', version: pnpmVersion };
241
373
  }
242
- // Format and lint code
374
+ }
375
+ if (userAgent.includes('pnpm/')) {
376
+ const pnpmVersion = await getPnpmVersion();
377
+ return { packageManager: 'pnpm', version: pnpmVersion };
378
+ }
379
+ // Check argv for direct invocations (fallback)
380
+ if (invocation.includes('bunx') || invocation.includes('bun create')) {
243
381
  try {
244
- execSync(`${targetPmCommand} format:fix`, { cwd: targetDir, stdio: 'ignore' });
245
- execSync(`${targetPmCommand} lint:fix`, { cwd: targetDir, stdio: 'ignore' });
382
+ const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
383
+ return { packageManager: 'bun', version: bunVersion };
246
384
  }
247
- catch (error) {
248
- finalizeSpinner.warn('Code formatting/linting skipped (optional)');
385
+ catch {
386
+ console.log(chalk.yellow('⚠️ Bun not available, falling back to pnpm'));
387
+ const pnpmVersion = await getPnpmVersion();
388
+ return { packageManager: 'pnpm', version: pnpmVersion };
249
389
  }
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' });
256
- }
257
- catch (error) {
258
- finalizeSpinner.warn('Git initialization failed (optional)');
259
- }
260
- }
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);
390
+ }
391
+ if (invocation.includes('pnpm create') || invocation.includes('pnpm dlx')) {
392
+ const pnpmVersion = await getPnpmVersion();
393
+ return { packageManager: 'pnpm', version: pnpmVersion };
394
+ }
395
+ // For npx (npm user agent) or unknown, ask user to choose
396
+ if (userAgent.includes('npm/') || invocation.includes('npx')) {
397
+ return await promptForPackageManager();
398
+ }
399
+ // Fallback: try to detect what's available
400
+ try {
401
+ const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
402
+ return { packageManager: 'bun', version: bunVersion };
403
+ }
404
+ catch {
405
+ const pnpmVersion = await getPnpmVersion();
406
+ return { packageManager: 'pnpm', version: pnpmVersion };
407
+ }
273
408
  }
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;
409
+ /**
410
+ * Get pnpm version with fallback
411
+ */
412
+ async function getPnpmVersion() {
413
+ try {
414
+ return execSync('pnpm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
415
+ }
416
+ catch {
417
+ return '9.0.0'; // Default version
418
+ }
419
+ }
420
+ /**
421
+ * Prompt user to choose package manager (for npx invocation)
422
+ */
423
+ async function promptForPackageManager() {
424
+ const inquirer = await import('inquirer');
425
+ // Check what's available
426
+ const bunAvailable = await checkCommandAvailable('bun');
427
+ const pnpmAvailable = await checkCommandAvailable('pnpm');
428
+ if (!bunAvailable && !pnpmAvailable) {
429
+ console.log(chalk.yellow('⚠️ Neither bun nor pnpm found, defaulting to pnpm'));
430
+ return { packageManager: 'pnpm', version: '9.0.0' };
431
+ }
432
+ const choices = [];
433
+ if (pnpmAvailable)
434
+ choices.push({ name: 'pnpm (recommended for most projects)', value: 'pnpm' });
435
+ if (bunAvailable)
436
+ choices.push({ name: 'bun (fast and modern)', value: 'bun' });
437
+ const { packageManager } = await inquirer.default.prompt([{
438
+ type: 'list',
439
+ name: 'packageManager',
440
+ message: 'Which package manager would you like to use?',
441
+ choices,
442
+ default: 'pnpm'
443
+ }]);
444
+ if (packageManager === 'bun') {
445
+ const bunVersion = execSync('bun --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
446
+ return { packageManager: 'bun', version: bunVersion };
447
+ }
448
+ else {
449
+ const pnpmVersion = await getPnpmVersion();
450
+ return { packageManager: 'pnpm', version: pnpmVersion };
451
+ }
452
+ }
453
+ /**
454
+ * Check if a command is available
455
+ */
456
+ async function checkCommandAvailable(command) {
457
+ try {
458
+ execSync(`${command} --version`, { stdio: 'pipe' });
459
+ return true;
460
+ }
461
+ catch {
462
+ return false;
463
+ }
464
+ }
465
+ /**
466
+ * Create project from template
467
+ */
468
+ export async function createProject(projectName, options = {}) {
469
+ const targetDir = path.join(process.cwd(), projectName);
470
+ const selectedTemplate = options.template || 'core';
471
+ // Validate target directory
472
+ if (fs.existsSync(targetDir)) {
473
+ throw new Error(`Directory ${projectName} already exists`);
474
+ }
475
+ if (!options.templatePath && !process.env.MVPKIT_TEMPLATE_PATH) {
476
+ validateTemplateName(selectedTemplate);
477
+ }
478
+ // Build initial template variables
479
+ const variables = {
480
+ template: selectedTemplate,
481
+ name: projectName,
482
+ description: options.description || `A full-stack application built with MVPKit`,
483
+ domain: options.domain || `${projectName}.localhost`,
484
+ executionDate: new Date().toISOString().slice(0, 10),
485
+ tagline: 'Build and deploy MVPs faster.',
486
+ social: {
487
+ github: {
488
+ handle: '@mvp-kit',
489
+ url: 'https://github.com/mvp-kit'
490
+ },
491
+ x: {
492
+ handle: '@mvpkithq',
493
+ url: 'https://x.com/mvpkithq'
494
+ },
495
+ instagram: {
496
+ handle: '@mvpkit',
497
+ url: 'https://instagram.com/mvpkit'
498
+ },
499
+ tiktok: {
500
+ handle: '@mvpkittok',
501
+ url: 'https://tiktok.com/@mvpkittok'
303
502
  }
304
503
  },
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
504
+ packageManager: 'pnpm', // Default, will be updated by prompts if needed
505
+ packageManagerVersion: '9.0.0' // Default, will be updated by prompts if needed
334
506
  };
335
- return { updatedVariables, updatedOptions };
507
+ // CLI mode with interactive prompts
508
+ // Collect missing variables interactively
509
+ const finalVariables = await collectVariables(variables, options);
510
+ const templateLabel = (finalVariables.template || 'core').toUpperCase();
511
+ console.log(chalk.blue(`🚀 Creating ${projectName} with MVPKit ${templateLabel}\n`));
512
+ try {
513
+ // Resolve template path once to avoid duplicate downloads/work.
514
+ const resolvedTemplatePath = await getTemplatePath({ ...options, template: finalVariables.template });
515
+ const resolvedOptions = {
516
+ ...options,
517
+ template: finalVariables.template,
518
+ templatePath: resolvedTemplatePath
519
+ };
520
+ // Copy template and complete setup using shared functionality
521
+ await copyTemplate(targetDir, finalVariables, resolvedOptions);
522
+ await completeProjectSetup(targetDir, finalVariables, resolvedOptions);
523
+ // Success!
524
+ console.log('\n' + chalk.green('🎉 Project Created Successfully!'));
525
+ console.log(chalk.white.dim('═'.repeat(50)));
526
+ console.log(chalk.green(`✅ ${projectName} is ready to go!`));
527
+ // Next steps
528
+ const extendedOptions = { ...options, componentPack: finalVariables.componentPack };
529
+ printNextSteps(projectName, extendedOptions, finalVariables.packageManager);
530
+ }
531
+ catch (error) {
532
+ // Clean up on failure
533
+ if (fs.existsSync(targetDir)) {
534
+ await fs.remove(targetDir);
535
+ }
536
+ throw error;
537
+ }
336
538
  }
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');
539
+ /**
540
+ * Initialize shadcn/ui ONLY in packages/ui using the official shadcn CLI
541
+ */
542
+ async function initShadcnInUIPackage(uiPath, style, interactive) {
543
+ const viteConfigPath = path.join(uiPath, 'vite.config.ts');
544
+ try {
545
+ // Create temporary vite.config.ts for framework detection
546
+ const viteConfig = `import { defineConfig } from 'vite'
547
+ import react from '@vitejs/plugin-react'
548
+
549
+ export default defineConfig({
550
+ plugins: [react()],
551
+ })
552
+ `;
553
+ fs.writeFileSync(viteConfigPath, viteConfig);
554
+ if (interactive) {
555
+ // In interactive mode, let users choose through prompts, force override existing config
556
+ execSync('npx shadcn@latest init -f -s', {
557
+ cwd: uiPath,
558
+ stdio: 'inherit',
559
+ timeout: 60000 // 60 second timeout for CLI init
560
+ });
357
561
  }
358
- // Skip files we don't want to copy
359
- if (shouldSkipFile(relativePath, skipPatterns)) {
360
- continue;
562
+ else {
563
+ // In non-interactive mode, use specific defaults (new-york, zinc) with -y flag and force override
564
+ execSync('npx shadcn@latest init -y -f -s -b zinc', {
565
+ cwd: uiPath,
566
+ stdio: 'pipe',
567
+ timeout: 60000 // 60 second timeout for CLI init
568
+ });
361
569
  }
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);
570
+ }
571
+ catch (error) {
572
+ throw new Error(`Failed to initialize shadcn/ui in UI package: ${error instanceof Error ? error.message : String(error)}`);
573
+ }
574
+ finally {
575
+ // Clean up temporary vite.config.ts
576
+ if (fs.existsSync(viteConfigPath)) {
577
+ fs.unlinkSync(viteConfigPath);
369
578
  }
370
- else {
371
- // For regular files, just transform filename
372
- targetRelativePath = transformFileName(relativePath, variables);
579
+ }
580
+ }
581
+ /**
582
+ * Install a component pack in the UI package
583
+ */
584
+ async function installComponentPack(uiPath, packName, interactive, templateConfig) {
585
+ const spinner = ora(` Adding ${packName} component library...`).start();
586
+ try {
587
+ if (packName === 'all') {
588
+ // Use shadcn --all flag for bulk installation
589
+ const addArgs = [
590
+ 'add',
591
+ interactive ? '' : '-y',
592
+ '--overwrite',
593
+ '--all'
594
+ ].filter(Boolean);
595
+ execSync(`npx shadcn@latest ${addArgs.join(' ')}`, {
596
+ cwd: uiPath,
597
+ stdio: 'pipe',
598
+ timeout: 120000 // 2 minute timeout for bulk install
599
+ });
373
600
  }
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
- }
601
+ else {
602
+ // Use template config if available, otherwise use default component packs
603
+ const componentPacks = templateConfig?.componentPacks || {
604
+ basic: { components: ['button', 'card', 'input', 'label', 'separator'] }
605
+ };
606
+ if (!(packName in componentPacks)) {
607
+ throw new Error(`Unknown component pack: ${packName}. Available packs: ${Object.keys(componentPacks).join(', ')}`);
404
608
  }
405
- else {
406
- // Write transformed content
407
- await fs.writeFile(targetFile, transformedContent);
609
+ const components = componentPacks[packName].components;
610
+ // Install components individually
611
+ for (const component of components) {
612
+ const addArgs = [
613
+ 'add',
614
+ interactive ? '' : '-y',
615
+ '--overwrite',
616
+ component
617
+ ].filter(Boolean);
618
+ execSync(`npx shadcn@latest ${addArgs.join(' ')}`, {
619
+ cwd: uiPath,
620
+ stdio: 'pipe',
621
+ timeout: 30000 // 30 second timeout per component
622
+ });
408
623
  }
409
624
  }
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
- }
625
+ spinner.succeed(` ✅ ${packName} component library ready`);
414
626
  }
415
- // Create additional setup files
416
- if (packageManagerInfo) {
417
- await createSetupFiles(targetDir, variables, packageManagerInfo);
627
+ catch (error) {
628
+ spinner.fail(` ❌ ${packName} component library installation failed`);
629
+ throw new Error(`Failed to install ${packName} component library: ${error instanceof Error ? error.message : String(error)}`);
418
630
  }
419
631
  }
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);
632
+ /**
633
+ * Inherit UI package configuration to web app components.json
634
+ * Web app gets config but NOT styles.css (styles come from UI package)
635
+ */
636
+ async function inheritUIConfigToWeb(uiPath, webPath) {
637
+ const uiComponentsPath = path.join(uiPath, 'components.json');
638
+ const webComponentsPath = path.join(webPath, 'components.json');
639
+ if (!fs.existsSync(uiComponentsPath)) {
640
+ throw new Error('UI package components.json not found - cannot inherit configuration');
641
+ }
642
+ try {
643
+ const uiConfig = JSON.parse(fs.readFileSync(uiComponentsPath, 'utf-8'));
644
+ const webConfigCurrent = fs.existsSync(webComponentsPath)
645
+ ? JSON.parse(fs.readFileSync(webComponentsPath, 'utf-8'))
646
+ : {};
647
+ // Create web-specific config inheriting style/theme from UI while preserving web paths.
648
+ const webConfig = {
649
+ ...webConfigCurrent,
650
+ style: uiConfig.style ?? webConfigCurrent.style,
651
+ rsc: uiConfig.rsc ?? webConfigCurrent.rsc,
652
+ tsx: uiConfig.tsx ?? webConfigCurrent.tsx,
653
+ tailwind: {
654
+ ...(webConfigCurrent.tailwind || {}),
655
+ ...uiConfig.tailwind,
656
+ css: webConfigCurrent?.tailwind?.css || 'src/styles.css'
657
+ },
658
+ aliases: {
659
+ ...(uiConfig.aliases || {}),
660
+ ...(webConfigCurrent.aliases || {})
661
+ },
662
+ registries: webConfigCurrent.registries || {}
663
+ };
664
+ await fs.writeJson(webComponentsPath, webConfig, { spaces: 2 });
665
+ }
666
+ catch (error) {
667
+ throw new Error(`Failed to inherit UI config to web: ${error instanceof Error ? error.message : String(error)}`);
668
+ }
519
669
  }
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;
670
+ /**
671
+ * Copy template files and process .template files
672
+ */
673
+ export async function copyTemplate(targetDir, variables, options = {}) {
674
+ const spinner = ora('Setting up project structure...').start();
529
675
  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
- });
676
+ const coreDir = await getTemplatePath(options);
677
+ // Get all files from core directory
678
+ const files = await glob('**/*', {
679
+ cwd: coreDir,
680
+ dot: true,
681
+ nodir: true,
682
+ absolute: false
683
+ });
684
+ // Process each file
685
+ for (const sourceFile of files) {
686
+ const sourcePath = path.join(coreDir, sourceFile);
687
+ // Skip unwanted files based on .templateignore
688
+ if (shouldSkipFile(sourceFile, coreDir)) {
689
+ continue;
690
+ }
691
+ // Determine target path
692
+ const targetFile = path.join(targetDir, getTargetFilePath(sourceFile));
693
+ // Ensure target directory exists
694
+ await fs.ensureDir(path.dirname(targetFile));
695
+ // Process template files
696
+ if (sourceFile.endsWith('.template')) {
697
+ const content = await fs.readFile(sourcePath, 'utf8');
698
+ const processedContent = processTemplate(content, variables);
699
+ await fs.writeFile(targetFile, processedContent);
538
700
  }
539
701
  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
- });
702
+ // Copy regular files as-is
703
+ await fs.copy(sourcePath, targetFile);
548
704
  }
549
705
  }
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
706
+ spinner.succeed('Project structure created');
707
+ }
708
+ catch (error) {
709
+ spinner.fail('Project structure creation failed');
710
+ throw error;
711
+ }
712
+ }
713
+ /**
714
+ * Copy package manager specific files from _packageManagers directory
715
+ */
716
+ export async function copyPackageManagerFiles(targetDir, variables, options = {}) {
717
+ const spinner = ora('Configuring package manager...').start();
718
+ try {
719
+ const coreDir = await getTemplatePath(options);
720
+ const packageManagerDir = path.join(coreDir, '_packageManagers', variables.packageManager);
721
+ // Check if package manager directory exists
722
+ if (!fs.existsSync(packageManagerDir)) {
723
+ spinner.succeed('Package manager configuration complete');
724
+ return;
560
725
  }
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
- });
726
+ // Get all files from package manager directory
727
+ const files = await glob('**/*', {
728
+ cwd: packageManagerDir,
729
+ dot: true,
730
+ nodir: true,
731
+ absolute: false
732
+ });
733
+ // Copy each file to the target directory root
734
+ for (const file of files) {
735
+ const sourcePath = path.join(packageManagerDir, file);
736
+ const targetFile = path.join(targetDir, file);
737
+ // Ensure target directory exists
738
+ await fs.ensureDir(path.dirname(targetFile));
739
+ // Process template files if they have .template extension
740
+ if (file.endsWith('.template')) {
741
+ const content = await fs.readFile(sourcePath, 'utf8');
742
+ const processedContent = processTemplate(content, variables);
743
+ const finalTargetFile = targetFile.replace(/\.template$/, '');
744
+ await fs.writeFile(finalTargetFile, processedContent);
570
745
  }
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
- }
746
+ else {
747
+ // Copy regular files as-is
748
+ await fs.copy(sourcePath, targetFile);
583
749
  }
584
750
  }
585
- componentSpinner?.succeed('Components installed');
751
+ spinner.succeed(`Configured for ${variables.packageManager}`);
586
752
  }
587
753
  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}`));
754
+ spinner.fail('Package manager configuration failed');
755
+ console.log(chalk.yellow(` Warning: ${error instanceof Error ? error.message : String(error)}`));
592
756
  }
593
757
  }
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();
758
+ /**
759
+ * Install dependencies
760
+ */
761
+ async function installDependencies(targetDir, packageManager) {
762
+ const spinner = ora('Installing packages...').start();
763
+ try {
764
+ const command = packageManager === 'bun' ? 'bun install' : 'pnpm install';
765
+ execSync(command, {
766
+ cwd: targetDir,
767
+ stdio: 'pipe'
768
+ });
769
+ spinner.succeed('All packages installed');
770
+ }
771
+ catch (error) {
772
+ spinner.fail('Package installation failed');
773
+ console.log(chalk.yellow(`\n💡 Install packages manually: cd ${path.basename(targetDir)} && ${packageManager} install\n`));
774
+ }
775
+ }
776
+ /**
777
+ * Setup database with Drizzle ORM
778
+ */
779
+ async function setupDatabase(apiPath, packageManager) {
780
+ const spinner = ora('Setting up database...').start();
781
+ try {
782
+ const pmCommand = packageManager === 'bun' ? 'bun' : 'pnpm';
783
+ // Initialize local database
784
+ execSync(`${pmCommand} run db:init`, {
785
+ cwd: apiPath,
786
+ stdio: 'pipe',
787
+ timeout: 30000
788
+ });
789
+ // Generate database schema
790
+ execSync(`${pmCommand} run db:generate`, {
791
+ cwd: apiPath,
792
+ stdio: 'pipe',
793
+ timeout: 60000
794
+ });
795
+ // Run local migrations
796
+ execSync(`${pmCommand} run db:migrate:local`, {
797
+ cwd: apiPath,
798
+ stdio: 'pipe',
799
+ timeout: 60000
800
+ });
801
+ spinner.succeed('Database setup completed');
802
+ }
803
+ catch (error) {
804
+ spinner.fail('Database setup failed');
805
+ console.log(chalk.yellow(`\n💡 Setup database manually: cd ${path.basename(apiPath)} && ${packageManager} run db:generate && ${packageManager} run db:migrate:local\n`));
806
+ }
807
+ }
808
+ /**
809
+ * Run lint:fix command if available
810
+ */
811
+ async function runLintFixCommand(targetDir, packageManager) {
812
+ const spinner = ora('Fixing code formatting...').start();
813
+ try {
814
+ const pmCommand = packageManager === 'pnpm' ? 'pnpm' : 'bun';
815
+ execSync(`${pmCommand} run lint:fix`, {
816
+ cwd: targetDir,
817
+ stdio: 'pipe',
818
+ timeout: 60000 // 60 second timeout
819
+ });
820
+ spinner.succeed('Code formatted and linted');
821
+ }
822
+ catch (error) {
823
+ // If lint:fix fails or doesn't exist, try just lint
824
+ try {
825
+ const pmCommand = packageManager === 'pnpm' ? 'pnpm' : 'bun';
826
+ execSync(`${pmCommand} run lint`, {
827
+ cwd: targetDir,
828
+ stdio: 'pipe',
829
+ timeout: 60000
830
+ });
831
+ spinner.succeed('Code formatting verified');
832
+ }
833
+ catch (lintError) {
834
+ // If no lint command exists, skip silently
835
+ spinner.succeed('Code formatting completed');
836
+ }
837
+ }
838
+ }
839
+ /**
840
+ * Initialize git repository with commit (after lint:fix)
841
+ */
842
+ async function initializeGitWithCommit(targetDir) {
843
+ const spinner = ora('Initializing version control...').start();
844
+ try {
845
+ execSync('git init', { cwd: targetDir, stdio: 'pipe' });
846
+ execSync('git add .', { cwd: targetDir, stdio: 'pipe' });
847
+ execSync('git commit -m "Initial commit from MVPKit Create CLI"', { cwd: targetDir, stdio: 'pipe' });
848
+ spinner.succeed('Version control initialized with initial commit');
849
+ }
850
+ catch (error) {
851
+ spinner.fail('Version control initialization failed');
852
+ console.log(chalk.yellow('\n💡 Initialize version control manually if needed\n'));
853
+ }
854
+ }
855
+ /**
856
+ * Initialize git repository (legacy function for compatibility)
857
+ */
858
+ async function initializeGit(targetDir) {
859
+ const spinner = ora('Initializing version control...').start();
860
+ try {
861
+ execSync('git init', { cwd: targetDir, stdio: 'pipe' });
862
+ execSync('git add .', { cwd: targetDir, stdio: 'pipe' });
863
+ execSync('git commit -m "Initial commit"', { cwd: targetDir, stdio: 'pipe' });
864
+ spinner.succeed('Version control initialized');
865
+ }
866
+ catch (error) {
867
+ spinner.fail('Version control initialization failed');
868
+ console.log(chalk.yellow('\n💡 Initialize version control manually if needed\n'));
869
+ }
870
+ }
871
+ /**
872
+ * Print next steps
873
+ */
874
+ function printNextSteps(projectName, options, packageManager) {
875
+ const pmCommand = packageManager === 'bun' ? 'bun' : 'pnpm';
876
+ console.log('\n' + chalk.cyan('🚀 Get Started'));
877
+ console.log(chalk.white.dim('═'.repeat(50)));
878
+ // Step-by-step instructions
879
+ console.log(chalk.bold('Next steps:'));
880
+ console.log(chalk.green(` 1. cd ${projectName}`));
881
+ if (options.install === false) {
882
+ console.log(chalk.green(` 2. ${pmCommand} install`));
883
+ console.log(chalk.green(` 3. ${pmCommand} dev`));
884
+ }
885
+ else {
886
+ console.log(chalk.green(` 2. ${pmCommand} dev`));
887
+ }
888
+ console.log(chalk.green(` 3. Open http://localhost:3001 (web) and http://localhost:3002 (app)`));
889
+ // What's included section
890
+ console.log('\n' + chalk.whiteBright.bold('🎁 What\'s Included:'));
891
+ const features = [
892
+ '⚡ Full-stack TypeScript with hot reload',
893
+ '🗄️ Database with Drizzle ORM + migrations',
894
+ '🎨 shadcn/ui components with new-york theme',
895
+ '🔐 Authentication with better-auth',
896
+ '🌐 Dual surfaces: apps/web (marketing) + apps/app (authenticated app)',
897
+ '📦 Monorepo structure (apps + packages + services)',
898
+ '🔧 ESLint, Prettier, and TypeScript configured'
899
+ ];
900
+ if (options.componentPack && options.componentPack === 'all') {
901
+ features.splice(2, 1, `🎨 shadcn/ui complete component library + new-york theme`);
902
+ }
903
+ else if (options.componentPack) {
904
+ features.splice(2, 1, `🎨 shadcn/ui essentials component pack + new-york theme`);
905
+ }
906
+ features.forEach(feature => {
907
+ console.log(chalk.white(` ${feature}`));
908
+ });
909
+ // Quick tips
910
+ console.log('\n' + chalk.whiteBright.bold('💡 Quick Tips:'));
911
+ console.log(chalk.white(' • Database is already set up and migrated'));
912
+ console.log(chalk.white(' • Add more components: npx shadcn@latest add <component>'));
913
+ console.log(chalk.white(' • Customize UI theme in packages/ui/src/styles.css'));
914
+ console.log(chalk.white(' • API handlers are in services/api/src/trpc/ and services/api/src/app.ts'));
915
+ console.log('\n' + chalk.bold('📚 Learn More:'));
916
+ console.log(chalk.blue(' https://docs.mvpkit.dev'));
917
+ console.log(chalk.white(' https://ui.shadcn.com (for UI components)'));
918
+ console.log(chalk.white(' https://orm.drizzle.team (for database)'));
919
+ console.log(chalk.white(''));
628
920
  }
629
921
  //# sourceMappingURL=create.js.map