UNPKG

@sprouted/create-vibes

Version:
378 lines (325 loc) 11.2 kB
#!/usr/bin/env node import { program } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const TEMPLATES_DIR = path.join(__dirname, 'templates'); // ASCII Art Logo const logo = ` ╭──────────────╮ │ VIBES │ │ ∿ │ │ ∿ ∿ │ │ ∿ ∿ ∿ │ ╰──────────────╯ `; async function createVibesProject(projectName, options) { const isCI = process.env.CI === 'true'; if (!isCI) { console.log(chalk.blue(logo)); console.log(chalk.blue.bold('Welcome to Vibes! 🌊\n')); } // Determine project path const projectPath = path.resolve(projectName); // Check if directory exists if (fs.existsSync(projectPath)) { console.error(chalk.red(`Error: Directory ${projectName} already exists!`)); process.exit(1); } // Interactive prompts if needed const answers = await inquirer.prompt([ { type: 'list', name: 'packageManager', message: 'Which package manager do you want to use?', choices: ['pnpm', 'npm', 'yarn'], default: 'pnpm', when: !options.packageManager }, { type: 'list', name: 'setupType', message: 'How would you like to start?', choices: [ { name: 'Bare (just the structure)', value: 'bare' }, { name: 'With example features', value: 'examples' }, { name: 'With starter apps', value: 'apps' }, { name: 'Full setup (apps + examples)', value: 'full' } ], default: 'bare', when: !options.bare && !options.examples && !options.apps } ]); // Handle the setup type let setupType = 'bare'; if (options.bare) setupType = 'bare'; else if (options.examples && options.apps) setupType = 'full'; else if (options.examples) setupType = 'examples'; else if (options.apps) setupType = 'apps'; else setupType = answers.setupType; // Additional prompts based on setup type if (setupType === 'apps' || setupType === 'full') { const appAnswers = await inquirer.prompt([ { type: 'checkbox', name: 'apps', message: 'Which starter apps would you like?', choices: [ { name: 'Mobile (Expo)', value: 'mobile' }, { name: 'Web (Vite + React)', value: 'web' }, { name: 'API (Go)', value: 'api' } ], default: ['mobile', 'api'], when: !options.apps } ]); answers.apps = appAnswers.apps; } // Merge options with answers const config = { projectName, projectPath, packageManager: options.packageManager || answers.packageManager, setupType, includeExamples: setupType === 'examples' || setupType === 'full', apps: setupType === 'apps' || setupType === 'full' ? (options.apps ? options.apps.split(',') : answers.apps) : [], skipInstall: options.skipInstall || false, git: options.git !== false }; // Create project const spinner = isCI ? { text: '', start: () => console.log('Creating your Vibes project...'), succeed: (msg) => console.log(msg), fail: (msg) => console.error(msg) } : ora('Creating your Vibes project...').start(); try { // Create base structure await createBaseStructure(config); // Install dependencies (only for root) if (!config.skipInstall) { spinner.text = 'Installing dependencies...'; await installDependencies(config); } // Create selected apps if (config.apps.length > 0) { for (const app of config.apps) { spinner.text = `Creating ${app} app...`; await createApp(app, config); } } // Create example features if requested if (config.includeExamples) { spinner.text = 'Creating example features...'; await createExampleFeatures(config); } // Initialize git if (config.git) { spinner.text = 'Initializing git...'; await initializeGit(config); } spinner.succeed(chalk.green('Project created successfully!')); // Show next steps (simpler in CI) if (!isCI) { console.log('\n' + chalk.blue.bold('🎉 Your Vibes project is ready!')); console.log('\nNext steps:'); console.log(chalk.cyan(` cd ${projectName}`)); console.log(chalk.cyan(` ./vibe create <feature-name> # Create your first feature`)); console.log(chalk.cyan(` ./vibe add <app-type> # Add an app when ready`)); if (config.setupType !== 'bare') { console.log(chalk.cyan(` ${config.packageManager} dev # Start development`)); } console.log('\n' + chalk.blue('Happy vibing! 🌊')); } else { console.log('Project created successfully!'); } } catch (error) { spinner.fail(chalk.red('Failed to create project')); console.error(error); process.exit(1); } } async function createBaseStructure(config) { const { projectPath } = config; // Create directories const dirs = [ 'apps', 'features', 'shared', 'tools', 'docs', 'examples' ]; for (const dir of dirs) { await fs.ensureDir(path.join(projectPath, dir)); } // Copy base templates const baseTemplate = path.join(TEMPLATES_DIR, 'base'); await fs.copy(baseTemplate, projectPath); // Rename dotfiles that were renamed to avoid npm exclusion const dotfiles = [ { from: 'gitignore', to: '.gitignore' }, { from: 'cursorrules', to: '.cursorrules' } ]; for (const file of dotfiles) { const fromPath = path.join(projectPath, file.from); const toPath = path.join(projectPath, file.to); if (await fs.pathExists(fromPath)) { await fs.rename(fromPath, toPath); } } // Write vibes config with package manager choice const vibesrcPath = path.join(projectPath, '.vibesrc'); const vibesConfig = { packageManager: config.packageManager }; await fs.writeJson(vibesrcPath, vibesConfig, { spaces: 2 }); // Create README files for empty directories const readmeContent = { apps: '# Apps\n\nApplications go here. Use `vibe add <app-type>` to create one.', features: '# Features\n\nFeatures go here. Use `vibe create <feature-name>` to create one.', shared: '# Shared\n\nShared utilities and components go here.', examples: '# Examples\n\nExample implementations for reference.' }; for (const [dir, content] of Object.entries(readmeContent)) { await fs.writeFile( path.join(projectPath, dir, 'README.md'), content ); } // Update package.json with project name and package manager const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); packageJson.name = `@${config.projectName}/root`; // Set packageManager field based on user choice if (config.packageManager === 'pnpm') { packageJson.packageManager = 'pnpm@10.10.0'; } else if (config.packageManager === 'npm') { // npm doesn't require a packageManager field delete packageJson.packageManager; } else if (config.packageManager === 'yarn') { packageJson.packageManager = 'yarn@1.22.19'; } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async function installDependencies(config) { const { projectPath, packageManager } = config; const installCommand = { npm: 'npm install', yarn: 'yarn install', pnpm: 'pnpm install' }; execSync(installCommand[packageManager], { cwd: projectPath, stdio: 'inherit' }); } async function createApp(appType, config) { const { projectPath } = config; const appsDir = path.join(projectPath, 'apps'); switch (appType) { case 'mobile': execSync('npx create-expo-app todo-mobile', { cwd: appsDir, stdio: 'inherit' }); // Copy our custom metro config const metroConfig = path.join(TEMPLATES_DIR, 'apps', 'mobile', 'metro.config.js'); await fs.copy(metroConfig, path.join(appsDir, 'todo-mobile', 'metro.config.js')); break; case 'web': execSync('npm create vite@latest todo-web -- --template react-ts', { cwd: appsDir, stdio: 'inherit' }); break; case 'api': // Copy our Go API template const apiTemplate = path.join(TEMPLATES_DIR, 'apps', 'api'); await fs.copy(apiTemplate, path.join(appsDir, 'todo-api')); break; } } async function createExampleFeatures(config) { const { projectPath } = config; const featuresDir = path.join(projectPath, 'features'); // Copy example features const exampleFeatures = path.join(TEMPLATES_DIR, 'features'); await fs.copy(exampleFeatures, featuresDir); } async function initializeGit(config) { const { projectPath } = config; const isCI = process.env.CI === 'true'; try { execSync('git init', { cwd: projectPath, stdio: 'inherit' }); // Set git config in CI environments if (isCI) { execSync('git config user.email "ci@vibes.dev"', { cwd: projectPath, stdio: 'inherit' }); execSync('git config user.name "Vibes CI"', { cwd: projectPath, stdio: 'inherit' }); } // Initial commit execSync('git add -A', { cwd: projectPath, stdio: 'inherit' }); execSync('git commit -m "Initial Vibes setup 🌊"', { cwd: projectPath, stdio: 'inherit' }); } catch (error) { // If git init fails in CI, just continue if (isCI) { console.log('Git initialization skipped in CI'); } else { throw error; } } } // CLI Setup program .name('create-vibes') .description('Create a new Vibes monorepo') .version('0.1.0') .argument('[project-name]', 'Name of your project') .option('-p, --package-manager <manager>', 'Package manager to use (npm, yarn, pnpm)', 'pnpm') .option('--bare', 'Create bare monorepo structure (no apps or examples)') .option('--examples', 'Include example features') .option('--apps <apps>', 'Comma-separated list of apps to create (mobile,web,api)') .option('--no-git', 'Skip git initialization') .option('--skip-install', 'Skip installing dependencies') .action(async (projectName, options) => { if (!projectName) { const answers = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'What is your project name?', default: 'my-vibes-app', validate: (input) => { if (!input.trim()) return 'Project name is required'; if (!/^[a-z0-9-]+$/.test(input)) { return 'Project name can only contain lowercase letters, numbers, and hyphens'; } return true; } } ]); projectName = answers.projectName; } await createVibesProject(projectName, options); }); program.parse();